mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-26 19:37:42 +00:00
Compare commits
163 Commits
quick-sear
...
ha-input
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab966d039a | ||
|
|
d06321ed43 | ||
|
|
3c3d8d9974 | ||
|
|
4f39fa482d | ||
|
|
5d0fe3236c | ||
|
|
b86142ae50 | ||
|
|
5d2f3ee5e8 | ||
|
|
e3f7c631a7 | ||
|
|
49f9d95853 | ||
|
|
db3d7701b5 | ||
|
|
3e55acf531 | ||
|
|
f102618d9d | ||
|
|
a3c02b511d | ||
|
|
74111d248e | ||
|
|
f8161b3505 | ||
|
|
6070c1907a | ||
|
|
ce5991582c | ||
|
|
d17217fc90 | ||
|
|
86b4bd0013 | ||
|
|
108ba3abd6 | ||
|
|
d38a2894c4 | ||
|
|
4c70376a62 | ||
|
|
8d69bd1401 | ||
|
|
5dfecd3693 | ||
|
|
efd51d2234 | ||
|
|
668299c16a | ||
|
|
5e155a4030 | ||
|
|
809fa10135 | ||
|
|
1cbc38f231 | ||
|
|
9ed39bb523 | ||
|
|
4e3d66cf40 | ||
|
|
2eaad79d1c | ||
|
|
afef7a2c0f | ||
|
|
18d5224002 | ||
|
|
dbffdfeaca | ||
|
|
0a4b7917ab | ||
|
|
e1524358d9 | ||
|
|
8774f9c3fc | ||
|
|
f9a9aeacab | ||
|
|
b798fee116 | ||
|
|
b25f731f0f | ||
|
|
26a7372c5e | ||
|
|
70d3409d62 | ||
|
|
0711ecddab | ||
|
|
bcfaa67eba | ||
|
|
1b60e6e04e | ||
|
|
a1a634f6dc | ||
|
|
55f48fbb56 | ||
|
|
ca4d66b94c | ||
|
|
51fd2eedd9 | ||
|
|
434a7c2e93 | ||
|
|
b849fecf0b | ||
|
|
3a48e1996f | ||
|
|
8299386737 | ||
|
|
5e58ff476f | ||
|
|
758d955053 | ||
|
|
1efd5d26f0 | ||
|
|
36979f10cc | ||
|
|
812c59fcb4 | ||
|
|
0c34165bcf | ||
|
|
8c2bfbe9ce | ||
|
|
8f721d74e2 | ||
|
|
63782e6ef3 | ||
|
|
eaad2295a9 | ||
|
|
e74eee3d34 | ||
|
|
cc39010839 | ||
|
|
7f97425214 | ||
|
|
8fac6e63de | ||
|
|
2ac8fe2b21 | ||
|
|
45ca1b2cdc | ||
|
|
0667f1e789 | ||
|
|
db49678ccb | ||
|
|
2ca7e9f71e | ||
|
|
8d883450a8 | ||
|
|
2c136e00f5 | ||
|
|
6f82478598 | ||
|
|
1093bd890f | ||
|
|
456c638750 | ||
|
|
60ca50deb4 | ||
|
|
2064ab4141 | ||
|
|
d34c42e587 | ||
|
|
5da7bf6fba | ||
|
|
f05ff58d27 | ||
|
|
7b0a381d93 | ||
|
|
8b38e6d170 | ||
|
|
6daf0eb469 | ||
|
|
6f8f849af3 | ||
|
|
cafe0f62c6 | ||
|
|
721cf46ce5 | ||
|
|
4e087760ab | ||
|
|
8fcfd4be84 | ||
|
|
b03680a8ab | ||
|
|
7ab0622bec | ||
|
|
c5aad44768 | ||
|
|
20ee7e5dc7 | ||
|
|
32fdcc708e | ||
|
|
7dd9b3308e | ||
|
|
71b870be15 | ||
|
|
f08c5fa03a | ||
|
|
fca408ae23 | ||
|
|
f3a814e38a | ||
|
|
7b0e4651c4 | ||
|
|
d98e373f64 | ||
|
|
649516c9fa | ||
|
|
bbc4fb96b2 | ||
|
|
0ae639aeb0 | ||
|
|
0e7e41065e | ||
|
|
685843f112 | ||
|
|
5e1a99d94a | ||
|
|
d843349865 | ||
|
|
ec23164aa9 | ||
|
|
e74ef11101 | ||
|
|
a222f6a736 | ||
|
|
ef3dd16d45 | ||
|
|
5d4e1d205e | ||
|
|
1ee5ebbe75 | ||
|
|
59d705aa3d | ||
|
|
332e108dae | ||
|
|
3c15b29d0a | ||
|
|
130c708e23 | ||
|
|
588a14a8a7 | ||
|
|
a1ef6ad266 | ||
|
|
a6c1f87730 | ||
|
|
49252a3808 | ||
|
|
c7877fe38f | ||
|
|
e355a61d8f | ||
|
|
f2e19e51ce | ||
|
|
fd9ab8f561 | ||
|
|
faa1b3c98f | ||
|
|
acc4a84fc9 | ||
|
|
4d723dac37 | ||
|
|
f1d4d0ef98 | ||
|
|
88180a2708 | ||
|
|
258d87e3d5 | ||
|
|
55f22ba61a | ||
|
|
812f3ca8b9 | ||
|
|
7f880d11a0 | ||
|
|
6b2452c538 | ||
|
|
c2cbf8bd21 | ||
|
|
224bcece9c | ||
|
|
dc84b7698f | ||
|
|
bc22e6a9bd | ||
|
|
d44874783a | ||
|
|
8d1bb5c867 | ||
|
|
da1b528eee | ||
|
|
756138408a | ||
|
|
3c8f112565 | ||
|
|
2521f3dde4 | ||
|
|
56390aa01a | ||
|
|
9aac5b19da | ||
|
|
24afc3dc88 | ||
|
|
873c7b2947 | ||
|
|
648db4276b | ||
|
|
f86c3e7856 | ||
|
|
1d0251cc28 | ||
|
|
518cf87847 | ||
|
|
81a9216c44 | ||
|
|
f0e10e0058 | ||
|
|
5df8ea4f07 | ||
|
|
73f081f5cc | ||
|
|
f0d1db1da6 | ||
|
|
c658eb414b | ||
|
|
bac493b72b |
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
|
||||
31
.github/workflows/restrict-task-creation.yml
vendored
31
.github/workflows/restrict-task-creation.yml
vendored
@@ -5,9 +5,38 @@ on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
|
||||
jobs:
|
||||
check-authorization:
|
||||
add-no-stale:
|
||||
name: Add no-stale label
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To add labels to issues
|
||||
if: >-
|
||||
github.event.issue.type.name == 'Task'
|
||||
|| github.event.issue.type.name == 'Epic'
|
||||
|| github.event.issue.type.name == 'Opportunity'
|
||||
steps:
|
||||
- name: Add no-stale label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['no-stale']
|
||||
});
|
||||
|
||||
check-authorization:
|
||||
name: Check authorization
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To comment on, label, and close issues
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.type.name == 'Task'
|
||||
steps:
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
||||
@@ -21,8 +21,8 @@ type DialogType =
|
||||
| "basic"
|
||||
| "basic-subtitle-below"
|
||||
| "basic-subtitle-above"
|
||||
| "allow-mode-change"
|
||||
| "form"
|
||||
| "form-block-mode"
|
||||
| "actions"
|
||||
| "large"
|
||||
| "small";
|
||||
@@ -69,8 +69,8 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<ha-button @click=${this._handleOpenDialog("form")}
|
||||
>Adaptive dialog with form</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("form-block-mode")}
|
||||
>Adaptive dialog with form (block mode change)</ha-button
|
||||
<ha-button @click=${this._handleOpenDialog("allow-mode-change")}
|
||||
>Adaptive dialog with allow mode change</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("actions")}
|
||||
>Adaptive dialog with actions</ha-button
|
||||
@@ -164,27 +164,15 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "form-block-mode"}
|
||||
header-title="Adaptive dialog with form (block mode change)"
|
||||
header-subtitle="This form will not reset when the viewport size changes"
|
||||
block-mode-change
|
||||
.allowModeChange=${this._openDialog === "allow-mode-change"}
|
||||
header-title="Adaptive dialog with allow mode change"
|
||||
header-subtitle="Resize the window while this dialog is open"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<ha-form autofocus .schema=${SCHEMA}></ha-form>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@click=${this._handleClosed}
|
||||
slot="secondaryAction"
|
||||
variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button
|
||||
@click=${this._handleClosed}
|
||||
slot="primaryAction"
|
||||
variant="accent"
|
||||
>Submit</ha-button
|
||||
>
|
||||
</ha-dialog-footer>
|
||||
<div>
|
||||
This dialog can switch between dialog mode and bottom sheet mode
|
||||
while open.
|
||||
</div>
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
@@ -225,10 +213,9 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
The mode is determined automatically and updates when the window is
|
||||
resized. To prevent mode changes after the initial mount (useful for
|
||||
preventing form resets), use the <code>block-mode-change</code>
|
||||
attribute.
|
||||
By default, the mode is determined at mount time and then stays fixed
|
||||
while the dialog is open. To allow switching modes while the viewport
|
||||
changes, use the <code>allow-mode-change</code> attribute.
|
||||
</p>
|
||||
|
||||
<h3>Width</h3>
|
||||
@@ -399,10 +386,10 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Use the <code>block-mode-change</code> attribute when you want to
|
||||
prevent the dialog from switching modes after it's opened. This is
|
||||
especially useful for forms, as it prevents form data from being lost
|
||||
when users resize their browser window.
|
||||
Use the <code>allow-mode-change</code> attribute when you want the
|
||||
dialog to switch between modes as the viewport changes after opening.
|
||||
For forms, you can keep the default behavior to avoid resetting fields
|
||||
on resize.
|
||||
</p>
|
||||
|
||||
<h3>Example usage</h3>
|
||||
@@ -410,7 +397,6 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<pre><code><ha-adaptive-dialog
|
||||
.hass=\${this.hass}
|
||||
open
|
||||
width="medium"
|
||||
header-title="Dialog title"
|
||||
header-subtitle="Dialog subtitle"
|
||||
>
|
||||
@@ -427,23 +413,6 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ha-dialog-footer>
|
||||
</ha-adaptive-dialog></code></pre>
|
||||
|
||||
<p>Example with <code>block-mode-change</code> for forms:</p>
|
||||
|
||||
<pre><code><ha-adaptive-dialog
|
||||
.hass=\${this.hass}
|
||||
open
|
||||
header-title="Edit configuration"
|
||||
block-mode-change
|
||||
>
|
||||
<ha-form .schema=\${schema} .data=\${data}></ha-form>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button slot="secondaryAction" variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button slot="primaryAction" variant="accent">Save</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-adaptive-dialog></code></pre>
|
||||
|
||||
<h3>API</h3>
|
||||
|
||||
<p>
|
||||
@@ -521,12 +490,10 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>block-mode-change</code></td>
|
||||
<td><code>allow-mode-change</code></td>
|
||||
<td>
|
||||
When set, the mode is determined at mount time based on the
|
||||
current screen size, but subsequent mode changes are blocked.
|
||||
Useful for preventing forms from resetting when the viewport
|
||||
size changes.
|
||||
When set, the dialog can switch between modes as the viewport
|
||||
size changes while it is open.
|
||||
</td>
|
||||
<td><code>false</code></td>
|
||||
<td><code>false</code>, <code>true</code></td>
|
||||
|
||||
@@ -222,6 +222,9 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
ha-language-picker {
|
||||
min-width: 200px;
|
||||
}
|
||||
ha-alert p {
|
||||
text-align: unset;
|
||||
}
|
||||
|
||||
41
package.json
41
package.json
@@ -34,10 +34,10 @@
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/view": "6.39.12",
|
||||
"@codemirror/view": "6.39.15",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.2.1",
|
||||
"@formatjs/intl-datetimeformat": "7.2.2",
|
||||
"@formatjs/intl-displaynames": "7.2.1",
|
||||
"@formatjs/intl-durationformat": "0.10.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.1",
|
||||
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.0",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.3",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -68,7 +68,6 @@
|
||||
"@material/mwc-fab": "0.27.0",
|
||||
"@material/mwc-floating-label": "0.27.0",
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
|
||||
"@material/mwc-icon-button": "0.27.0",
|
||||
"@material/mwc-linear-progress": "0.27.0",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@material/mwc-radio": "0.27.0",
|
||||
@@ -84,7 +83,7 @@
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.18",
|
||||
"@swc/helpers": "0.5.19",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
@@ -119,7 +118,7 @@
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.1",
|
||||
"marked": "17.0.3",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -149,14 +148,14 @@
|
||||
"@babel/helper-define-polyfill-provider": "0.6.6",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.9",
|
||||
"@html-eslint/eslint-plugin": "0.55.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.10",
|
||||
"@html-eslint/eslint-plugin": "0.56.0",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.2",
|
||||
"@rspack/core": "1.7.5",
|
||||
"@rspack/core": "1.7.6",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -173,7 +172,7 @@
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.18",
|
||||
@@ -181,25 +180,25 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.1",
|
||||
"eslint": "9.39.2",
|
||||
"eslint": "9.39.3",
|
||||
"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": "2.2.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.3.0",
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
"eslint-plugin-wc": "3.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"glob": "13.0.1",
|
||||
"glob": "13.0.6",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "28.0.0",
|
||||
"jsdom": "28.1.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -211,12 +210,12 @@
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.1",
|
||||
"tar": "7.5.7",
|
||||
"tar": "7.5.9",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.54.0",
|
||||
"vite-tsconfig-paths": "6.0.5",
|
||||
"typescript-eslint": "8.56.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.0.18",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
@@ -236,6 +235,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.13.1"
|
||||
"node": "24.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export class HaAuthFormString extends HaFormString {
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -210,3 +210,39 @@ const formatDateWeekdayShortMem = memoizeOne(
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
})
|
||||
);
|
||||
|
||||
// Mon, Aug 10
|
||||
export const formatDateWeekdayVeryShortDate = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) =>
|
||||
formatDateWeekdayVeryShortDateMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayVeryShortDateMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
})
|
||||
);
|
||||
|
||||
// Mon, Aug 10, 2021
|
||||
export const formatDateWeekdayShortDate = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateWeekdayShortDateMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayShortDateMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -18,9 +18,11 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { getAllGraphColors } from "../../common/color/colors";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { themesContext } from "../../data/context";
|
||||
import type { Themes } from "../../data/ws-themes";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
@@ -28,8 +30,6 @@ import type { HomeAssistant } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../ha-icon-button";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { formatTimeLabel } from "./axis-label";
|
||||
import { downSampleLineData } from "./down-sample";
|
||||
|
||||
@@ -1115,13 +1115,13 @@ export class HaChartBase extends LitElement {
|
||||
.chart-controls ::slotted(ha-icon-button) {
|
||||
background: var(--card-background-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
--mdc-icon-button-size: 32px;
|
||||
--ha-icon-button-size: 32px;
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
}
|
||||
.chart-controls.small ha-icon-button,
|
||||
.chart-controls.small ::slotted(ha-icon-button) {
|
||||
--mdc-icon-button-size: 22px;
|
||||
--ha-icon-button-size: 22px;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
.chart-controls ha-icon-button.inactive,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
import { stateColorProperties } from "../../common/entity/state_color";
|
||||
import { slugify } from "../../common/string/slugify";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { computeCssValue } from "../../resources/css-variables";
|
||||
|
||||
@@ -32,6 +33,22 @@ function computeTimelineStateColor(
|
||||
return computeCssValue("--history-unknown-color", computedStyles);
|
||||
}
|
||||
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
||||
// Zone states for person/device_tracker don't have specific CSS color variables,
|
||||
// so they all fall back to the same --state-person-active-color.
|
||||
// Only use a custom CSS variable if explicitly defined (e.g. --state-person-kitchen-color),
|
||||
// otherwise return undefined to get unique colors from the generic color handler.
|
||||
if (
|
||||
(domain === "person" || domain === "device_tracker") &&
|
||||
!((FIXED_DOMAIN_STATES[domain] || []) as readonly string[]).includes(state)
|
||||
) {
|
||||
return computeCssValue(
|
||||
`--state-${domain}-${slugify(state, "_")}-color`,
|
||||
computedStyles
|
||||
);
|
||||
}
|
||||
|
||||
const properties = stateColorProperties(stateObj, state);
|
||||
|
||||
if (!properties) {
|
||||
@@ -41,8 +58,6 @@ function computeTimelineStateColor(
|
||||
const rgb = computeCssValue(properties, computedStyles);
|
||||
|
||||
if (!rgb) return undefined;
|
||||
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const shade = DOMAIN_STATE_SHADES[domain]?.[state] as number | number;
|
||||
if (!shade) {
|
||||
return rgb;
|
||||
|
||||
@@ -32,11 +32,13 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
@state() private _hiddenColumns?: string[];
|
||||
|
||||
private _lastFixedKeys: string[] = [];
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: DataTableSettingsDialogParams) {
|
||||
this._params = params;
|
||||
this._columnOrder = params.columnOrder;
|
||||
this._columnOrder = this._preserveLastFixed(params.columnOrder);
|
||||
this._hiddenColumns = params.hiddenColumns;
|
||||
this._open = true;
|
||||
}
|
||||
@@ -50,6 +52,29 @@ export class DialogDataTableSettings extends LitElement {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _lastFixedCount(): number {
|
||||
const lastFixedKeys = Object.keys(this._params!.columns).filter(
|
||||
(col) => this._params!.columns[col].lastFixed
|
||||
);
|
||||
if (lastFixedKeys.length) {
|
||||
this._lastFixedKeys = lastFixedKeys;
|
||||
}
|
||||
return lastFixedKeys.length;
|
||||
}
|
||||
|
||||
private _preserveLastFixed(columnOrder) {
|
||||
let strippedColumnOrder;
|
||||
const lastFixedCount = this._lastFixedCount();
|
||||
if (lastFixedCount && columnOrder) {
|
||||
strippedColumnOrder = [...columnOrder];
|
||||
strippedColumnOrder.splice(
|
||||
columnOrder.length - lastFixedCount,
|
||||
lastFixedCount
|
||||
);
|
||||
}
|
||||
return strippedColumnOrder;
|
||||
}
|
||||
|
||||
private _sortedColumns = memoizeOne(
|
||||
(
|
||||
columns: DataTableColumnContainer,
|
||||
@@ -57,7 +82,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
hiddenColumns: string[] | undefined
|
||||
) =>
|
||||
Object.keys(columns)
|
||||
.filter((col) => !columns[col].hidden)
|
||||
.filter((col) => !columns[col].hidden && !columns[col].lastFixed)
|
||||
.sort((a, b) => {
|
||||
const orderA = columnOrder?.indexOf(a) ?? -1;
|
||||
const orderB = columnOrder?.indexOf(b) ?? -1;
|
||||
@@ -195,7 +220,8 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
this._columnOrder = columnOrder;
|
||||
|
||||
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
|
||||
const reportedOrder = columnOrder.concat(this._lastFixedKeys);
|
||||
this._params!.onUpdate(reportedOrder, this._hiddenColumns);
|
||||
}
|
||||
|
||||
private _toggle(ev) {
|
||||
@@ -276,7 +302,8 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
this._hiddenColumns = hidden;
|
||||
|
||||
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
|
||||
const reportedOrder = this._columnOrder.concat(this._lastFixedKeys);
|
||||
this._params!.onUpdate(reportedOrder, this._hiddenColumns);
|
||||
}
|
||||
|
||||
private _reset() {
|
||||
|
||||
@@ -86,6 +86,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
||||
flex?: number;
|
||||
forceLTR?: boolean;
|
||||
hidden?: boolean;
|
||||
lastFixed?: boolean;
|
||||
}
|
||||
|
||||
export type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
|
||||
@@ -135,9 +136,6 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public searchLabel?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-label-float" })
|
||||
public noLabelFloat? = false;
|
||||
|
||||
@property({ type: String }) public filter = "";
|
||||
|
||||
@property({ attribute: false }) public groupColumn?: string;
|
||||
@@ -359,6 +357,11 @@ export class HaDataTable extends LitElement {
|
||||
.sort((a, b) => {
|
||||
const orderA = columnOrder!.indexOf(a);
|
||||
const orderB = columnOrder!.indexOf(b);
|
||||
const fixedA = Boolean(columns[a].lastFixed);
|
||||
const fixedB = Boolean(columns[b].lastFixed);
|
||||
if (fixedA !== fixedB) {
|
||||
return fixedA ? 1 : -1;
|
||||
}
|
||||
if (orderA !== orderB) {
|
||||
if (orderA === -1) {
|
||||
return 1;
|
||||
@@ -394,7 +397,6 @@ export class HaDataTable extends LitElement {
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.searchLabel}
|
||||
.noLabelFloat=${this.noLabelFloat}
|
||||
></search-input>
|
||||
</div>
|
||||
`
|
||||
@@ -428,9 +430,9 @@ export class HaDataTable extends LitElement {
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxClick}
|
||||
.indeterminate=${this._checkedRows.length &&
|
||||
.indeterminate=${!!this._checkedRows.length &&
|
||||
this._checkedRows.length !== this._checkableRowsCount}
|
||||
.checked=${this._checkedRows.length &&
|
||||
.checked=${!!this._checkedRows.length &&
|
||||
this._checkedRows.length === this._checkableRowsCount}
|
||||
>
|
||||
</ha-checkbox>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
@customElement("ha-entity-attribute-picker")
|
||||
class HaEntityAttributePicker extends LitElement {
|
||||
@@ -94,12 +95,19 @@ class HaEntityAttributePicker extends LitElement {
|
||||
.helper=${this.helper}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.getItems=${this._getItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||
const items = this._getItems();
|
||||
const item = items.find((option) => option.id === value);
|
||||
return html`<span slot="headline">${item?.primary ?? value}</span>`;
|
||||
};
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
@@ -164,7 +164,7 @@ export class HaEntityToggle extends LitElement {
|
||||
min-width: 38px;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 40px;
|
||||
--ha-icon-button-size: 40px;
|
||||
color: var(--ha-icon-button-inactive-color, var(--primary-text-color));
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { addBrandsAuth } from "../../util/brands-url";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@customElement("state-badge")
|
||||
@@ -137,6 +138,7 @@ export class StateBadge extends LitElement {
|
||||
let imageUrl =
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
imageUrl = addBrandsAuth(imageUrl);
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
|
||||
@@ -220,6 +220,7 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
return [
|
||||
css`
|
||||
ha-bottom-sheet {
|
||||
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
|
||||
--ha-bottom-sheet-surface-background: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--card-background-color, var(--ha-color-surface-default))
|
||||
|
||||
@@ -135,7 +135,7 @@ class HaAlert extends LitElement {
|
||||
}
|
||||
.action ha-icon-button {
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
--mdc-icon-button-size: 36px;
|
||||
--ha-icon-button-size: 36px;
|
||||
}
|
||||
.issue-type.info > .icon {
|
||||
color: var(--info-color);
|
||||
|
||||
@@ -672,11 +672,11 @@ export class HaAssistChat extends LitElement {
|
||||
--markdown-code-background-color: var(--primary-background-color);
|
||||
--markdown-code-text-color: var(--primary-text-color);
|
||||
--markdown-list-indent: 1.15em;
|
||||
&:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
ha-markdown:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bouncer {
|
||||
width: 48px;
|
||||
|
||||
@@ -4,10 +4,10 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-select";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@@ -368,7 +368,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
}
|
||||
ha-icon-button {
|
||||
position: relative;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -2,6 +2,7 @@ import "@home-assistant/webawesome/dist/components/drawer/drawer";
|
||||
import type WaDrawer from "@home-assistant/webawesome/dist/components/drawer/drawer";
|
||||
import { css, html, LitElement, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
@@ -11,6 +12,19 @@ import { isIosApp } from "../util/is_ios";
|
||||
|
||||
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
||||
|
||||
const SWIPE_LOCKED_COMPONENTS = new Set([
|
||||
"ha-control-slider",
|
||||
"ha-slider",
|
||||
"ha-control-switch",
|
||||
"ha-control-circular-slider",
|
||||
"ha-hs-color-picker",
|
||||
"ha-map",
|
||||
"ha-more-info-control-select-container",
|
||||
"ha-filter-chip",
|
||||
]);
|
||||
|
||||
const SWIPE_LOCKED_CLASSES = new Set(["volume-slider-container", "forecast"]);
|
||||
|
||||
@customElement("ha-bottom-sheet")
|
||||
export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@@ -31,6 +45,8 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@state() private _drawerOpen = false;
|
||||
|
||||
@state() private _sliderInteractionActive = false;
|
||||
|
||||
@query("#drawer") private _drawer!: HTMLElement;
|
||||
|
||||
@query("#body") private _bodyElement!: HTMLDivElement;
|
||||
@@ -78,26 +94,55 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
fireEvent(this, "after-show");
|
||||
};
|
||||
|
||||
private _handleAfterHide = () => {
|
||||
this.open = false;
|
||||
this._drawerOpen = false;
|
||||
fireEvent(this, "closed");
|
||||
private _handleSliderInteractionStart = () => {
|
||||
this._sliderInteractionActive = true;
|
||||
};
|
||||
|
||||
private _handleSliderInteractionStop = () => {
|
||||
this._sliderInteractionActive = false;
|
||||
};
|
||||
|
||||
private _handleAfterHide = (ev: CustomEvent<{ source: Element }>) => {
|
||||
if (this._sliderInteractionActive) {
|
||||
this._drawerOpen = true;
|
||||
this.open = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.eventPhase === Event.AT_TARGET) {
|
||||
this.open = false;
|
||||
this._drawerOpen = false;
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
};
|
||||
|
||||
private _handleHide = (ev: CustomEvent<{ source: Element }>) => {
|
||||
if (
|
||||
this.preventScrimClose &&
|
||||
this._escapePressed &&
|
||||
ev.detail.source === (ev.target as WaDrawer).drawer
|
||||
) {
|
||||
// Ignore bubbled wa-hide events from nested drawers (e.g., picker bottom sheet)
|
||||
if (ev.eventPhase !== Event.AT_TARGET) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceIsDrawer = ev.detail.source === (ev.target as WaDrawer).drawer;
|
||||
|
||||
if (this._sliderInteractionActive) {
|
||||
ev.preventDefault();
|
||||
this._drawerOpen = true;
|
||||
this.open = true;
|
||||
this._escapePressed = false;
|
||||
return;
|
||||
}
|
||||
if (this.preventScrimClose && this._escapePressed && sourceIsDrawer) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
this._escapePressed = false;
|
||||
};
|
||||
|
||||
private _handleKeyDown = (ev: KeyboardEvent) => {
|
||||
if (ev.key === "Escape") {
|
||||
this._escapePressed = true;
|
||||
ev.stopPropagation();
|
||||
(ev.currentTarget as WaDrawer).open = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -116,6 +161,24 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
};
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener(
|
||||
"slider-interaction-start",
|
||||
this._handleSliderInteractionStart,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
);
|
||||
this.addEventListener(
|
||||
"slider-interaction-stop",
|
||||
this._handleSliderInteractionStop,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("open")) {
|
||||
@@ -141,6 +204,9 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
without-header
|
||||
@touchstart=${this._handleTouchStart}
|
||||
>
|
||||
<div class="handle-wrapper" aria-hidden="true">
|
||||
<div class="handle"></div>
|
||||
</div>
|
||||
<slot name="header"></slot>
|
||||
<div class="content-wrapper">
|
||||
<div id="body" class="body ha-scrollbar">
|
||||
@@ -158,17 +224,33 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if any element inside drawer in the composed path has scrollTop > 0
|
||||
for (const path of ev.composedPath()) {
|
||||
const el = path as HTMLElement;
|
||||
if (el === this._drawer) {
|
||||
const path = ev.composedPath();
|
||||
|
||||
for (const target of path) {
|
||||
if (target === this._drawer) {
|
||||
break;
|
||||
}
|
||||
if (el.scrollTop > 0) {
|
||||
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
// Check if any element inside drawer in the composed path has scrollTop > 0 (list)
|
||||
target.scrollTop > 0 ||
|
||||
// Check if the element is a swipe locked component or has a swipe locked class
|
||||
SWIPE_LOCKED_COMPONENTS.has(target.localName) ||
|
||||
Array.from(target.classList).some((cls) =>
|
||||
SWIPE_LOCKED_CLASSES.has(cls)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop propagation so parent bottom sheets don't also start tracking
|
||||
// this gesture (same pattern as _handleKeyDown for Escape)
|
||||
ev.stopPropagation();
|
||||
this._startResizing(ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
@@ -264,6 +346,20 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener(
|
||||
"slider-interaction-start",
|
||||
this._handleSliderInteractionStart,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
);
|
||||
this.removeEventListener(
|
||||
"slider-interaction-stop",
|
||||
this._handleSliderInteractionStop,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
);
|
||||
this._unregisterResizeHandlers();
|
||||
this._isDragging = false;
|
||||
}
|
||||
@@ -289,6 +385,7 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
wa-drawer::part(body) {
|
||||
max-width: var(--ha-bottom-sheet-max-width);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
border-top-left-radius: var(
|
||||
--ha-bottom-sheet-border-radius,
|
||||
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
|
||||
@@ -311,6 +408,35 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
:host([prevent-scrim-close]) .handle-wrapper {
|
||||
display: none;
|
||||
}
|
||||
.handle-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
padding-bottom: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.handle-wrapper .handle {
|
||||
height: 16px;
|
||||
width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.handle-wrapper .handle::after {
|
||||
content: "";
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
height: 4px;
|
||||
background: var(--ha-bottom-sheet-handle-color, var(--divider-color));
|
||||
width: 40px;
|
||||
}
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
@@ -360,6 +486,18 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"slider-interaction-start": undefined;
|
||||
"slider-interaction-stop": undefined;
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"slider-interaction-start": HASSDomEvent<
|
||||
HASSDomEvents["slider-interaction-start"]
|
||||
>;
|
||||
"slider-interaction-stop": HASSDomEvent<
|
||||
HASSDomEvents["slider-interaction-stop"]
|
||||
>;
|
||||
}
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-bottom-sheet": HaBottomSheet;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export class HaButton extends Button {
|
||||
Button.styles,
|
||||
css`
|
||||
:host {
|
||||
--wa-form-control-padding-inline: 16px;
|
||||
--wa-form-control-padding-inline: var(--ha-space-4);
|
||||
--wa-font-weight-action: var(--ha-font-weight-medium);
|
||||
--wa-form-control-border-radius: var(
|
||||
--ha-button-border-radius,
|
||||
@@ -68,7 +68,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;
|
||||
--wa-form-control-padding-inline: var(--ha-space-3);
|
||||
}
|
||||
|
||||
:host([variant="brand"]) {
|
||||
@@ -84,6 +84,9 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-primary-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-primary-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="neutral"]) {
|
||||
@@ -99,6 +102,9 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-neutral-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-neutral-normal-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="success"]) {
|
||||
@@ -114,6 +120,9 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-success-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-success-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="warning"]) {
|
||||
@@ -129,6 +138,9 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-warning-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-warning-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="danger"]) {
|
||||
@@ -144,6 +156,9 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-danger-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-danger-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([appearance~="plain"]) .button {
|
||||
@@ -187,6 +202,10 @@ export class HaButton extends Button {
|
||||
background-color: var(--ha-color-fill-disabled-normal-resting);
|
||||
color: var(--ha-color-on-disabled-normal);
|
||||
}
|
||||
:host([appearance~="plain"])
|
||||
.button:not(.disabled):not(.loading):active {
|
||||
background-color: var(--button-color-fill-quiet-active);
|
||||
}
|
||||
|
||||
:host([appearance~="accent"]) .button {
|
||||
background-color: var(
|
||||
@@ -212,17 +231,17 @@ export class HaButton extends Button {
|
||||
}
|
||||
|
||||
slot[name="start"]::slotted(*) {
|
||||
margin-inline-end: 4px;
|
||||
margin-inline-end: var(--ha-space-1);
|
||||
}
|
||||
slot[name="end"]::slotted(*) {
|
||||
margin-inline-start: 4px;
|
||||
margin-inline-start: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.button.has-start {
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
}
|
||||
.button.has-end {
|
||||
padding-inline-end: 8px;
|
||||
padding-inline-end: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.label {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { STATE_RUNNING } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
@@ -58,12 +59,22 @@ export class HaCameraStream extends LitElement {
|
||||
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
if (
|
||||
const entityChanged =
|
||||
changedProps.has("stateObj") &&
|
||||
this.stateObj &&
|
||||
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
|
||||
this.stateObj.entity_id
|
||||
) {
|
||||
this.stateObj.entity_id;
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const backendStarted =
|
||||
changedProps.has("hass") &&
|
||||
this.hass &&
|
||||
this.stateObj &&
|
||||
oldHass &&
|
||||
this.hass.config.state === STATE_RUNNING &&
|
||||
oldHass.config?.state !== STATE_RUNNING;
|
||||
|
||||
if (entityChanged || backendStarted) {
|
||||
this._getCapabilities();
|
||||
this._getPosterUrl();
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export class HaCopyTextfield extends LitElement {
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -20,6 +20,8 @@ import "./ha-icon-button";
|
||||
|
||||
export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
|
||||
type DialogHideEvent = CustomEvent<{ source?: Element }>;
|
||||
|
||||
/**
|
||||
* Home Assistant dialog component
|
||||
*
|
||||
@@ -217,7 +219,7 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
fireEvent(this, "after-show");
|
||||
};
|
||||
|
||||
private _handleAfterHide = (ev: CustomEvent<{ source: Element }>) => {
|
||||
private _handleAfterHide = (ev: DialogHideEvent) => {
|
||||
if (ev.eventPhase === Event.AT_TARGET) {
|
||||
this._open = false;
|
||||
fireEvent(this, "closed");
|
||||
@@ -237,17 +239,18 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
private _handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Escape") {
|
||||
this._escapePressed = true;
|
||||
ev.stopPropagation();
|
||||
(ev.currentTarget as WaDialog).open = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleHide(ev: CustomEvent<{ source: Element }>) {
|
||||
if (
|
||||
this.preventScrimClose &&
|
||||
this._escapePressed &&
|
||||
ev.detail.source === (ev.target as WaDialog).dialog
|
||||
) {
|
||||
private _handleHide(ev: DialogHideEvent) {
|
||||
const sourceIsDialog = ev.detail?.source === (ev.target as WaDialog).dialog;
|
||||
|
||||
if (this.preventScrimClose && this._escapePressed && sourceIsDialog) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
this._escapePressed = false;
|
||||
}
|
||||
|
||||
@@ -281,8 +284,8 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
wa-dialog {
|
||||
--show-duration: 1ms;
|
||||
--hide-duration: 1ms;
|
||||
--show-duration: 0ms;
|
||||
--hide-duration: 0ms;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,29 +334,29 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host([type="standard"]) {
|
||||
--ha-dialog-border-radius: 0;
|
||||
}
|
||||
|
||||
wa-dialog {
|
||||
/* Make the container fill the whole screen width and not the safe width */
|
||||
--full-width: var(--ha-dialog-width-full, 100vw);
|
||||
--width: var(--full-width);
|
||||
}
|
||||
:host([type="standard"]) wa-dialog {
|
||||
/* Make the container fill the whole screen width and not the safe width */
|
||||
--full-width: var(--ha-dialog-width-full, 100vw);
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
/* Make the dialog fill the whole screen height and not the safe height */
|
||||
min-height: var(--ha-dialog-min-height, 100vh);
|
||||
min-height: var(--ha-dialog-min-height, 100dvh);
|
||||
max-height: var(--ha-dialog-max-height, 100vh);
|
||||
max-height: var(--ha-dialog-max-height, 100dvh);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
/* Use safe area as padding instead of the container size */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
/* Reset the transform to center the dialog */
|
||||
transform: none;
|
||||
}
|
||||
:host([type="standard"]) wa-dialog::part(dialog) {
|
||||
/* Make the dialog fill the whole screen height and not the safe height */
|
||||
min-height: var(--ha-dialog-min-height, 100vh);
|
||||
min-height: var(--ha-dialog-min-height, 100dvh);
|
||||
max-height: var(--ha-dialog-max-height, 100vh);
|
||||
max-height: var(--ha-dialog-max-height, 100dvh);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
/* Use safe area as padding instead of the container size */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
/* Reset the transform to center the dialog */
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type WaButton from "@home-assistant/webawesome/dist/components/button/button";
|
||||
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
||||
import { css, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HaDropdownItem } from "./ha-dropdown-item";
|
||||
import type { HaIconButton } from "./ha-icon-button";
|
||||
|
||||
/**
|
||||
* Event type for the ha-dropdown component when an item is selected.
|
||||
@@ -29,16 +29,25 @@ export class HaDropdown extends Dropdown {
|
||||
|
||||
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
|
||||
|
||||
public get anchorElement(): HTMLButtonElement | WaButton | undefined {
|
||||
public get anchorElement(): HTMLButtonElement | HaIconButton | undefined {
|
||||
// @ts-ignore Allow to set an anchor element on popup
|
||||
return this.popup?.anchor as HTMLButtonElement | WaButton | undefined;
|
||||
return this.popup?.anchor as HTMLButtonElement | HaIconButton | undefined;
|
||||
}
|
||||
|
||||
public set anchorElement(element: HTMLButtonElement | WaButton | undefined) {
|
||||
public set anchorElement(
|
||||
element: HTMLButtonElement | HaIconButton | undefined
|
||||
) {
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
if (!this.popup) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (this.popup.anchor && this.popup.anchor.localName === "ha-icon-button") {
|
||||
// @ts-ignore
|
||||
(this.popup.anchor as HaIconButton).selected = false;
|
||||
}
|
||||
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
this.popup.anchor = element;
|
||||
}
|
||||
@@ -46,7 +55,7 @@ export class HaDropdown extends Dropdown {
|
||||
/** Get the slotted trigger button, a <wa-button> or <button> element */
|
||||
// @ts-ignore Override parent method to be able to use alternative anchor
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override getTrigger(): HTMLButtonElement | WaButton | null {
|
||||
private override getTrigger(): HTMLButtonElement | HaIconButton | null {
|
||||
if (this.anchorElement) {
|
||||
return this.anchorElement;
|
||||
}
|
||||
@@ -54,6 +63,28 @@ export class HaDropdown extends Dropdown {
|
||||
return super.getTrigger();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override async showMenu() {
|
||||
// @ts-ignore
|
||||
await super.showMenu();
|
||||
const triggerElement = this.getTrigger();
|
||||
if (triggerElement && triggerElement.localName === "ha-icon-button") {
|
||||
(triggerElement as HaIconButton).selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override async hideMenu() {
|
||||
const triggerElement = this.getTrigger();
|
||||
if (triggerElement && triggerElement.localName === "ha-icon-button") {
|
||||
(triggerElement as HaIconButton).selected = false;
|
||||
}
|
||||
// @ts-ignore
|
||||
await super.hideMenu();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Dropdown.styles,
|
||||
|
||||
@@ -37,6 +37,9 @@ class HaDurationInput extends LitElement {
|
||||
@property({ attribute: "allow-negative", type: Boolean })
|
||||
public allowNegative = false;
|
||||
|
||||
@property({ attribute: "enable-second", type: Boolean })
|
||||
public enableSecond = true;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
private _toggleNegative = false;
|
||||
@@ -65,7 +68,7 @@ class HaDurationInput extends LitElement {
|
||||
.autoValidate=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
errorMessage="Required"
|
||||
enable-second
|
||||
.enableSecond=${this.enableSecond}
|
||||
.enableMillisecond=${this.enableMillisecond}
|
||||
.enableDay=${this.enableDay}
|
||||
format="24"
|
||||
@@ -162,9 +165,9 @@ class HaDurationInput extends LitElement {
|
||||
if (value) {
|
||||
value.hours ||= 0;
|
||||
value.minutes ||= 0;
|
||||
value.seconds ||= 0;
|
||||
|
||||
if ("days" in value) value.days ||= 0;
|
||||
if ("seconds" in value) value.seconds ||= 0;
|
||||
if ("milliseconds" in value) value.milliseconds ||= 0;
|
||||
|
||||
if (this.allowNegative) {
|
||||
@@ -183,8 +186,11 @@ class HaDurationInput extends LitElement {
|
||||
value.milliseconds %= 1000;
|
||||
}
|
||||
|
||||
if (value.seconds > 59) {
|
||||
value.minutes += Math.floor(value.seconds / 60);
|
||||
if (!this.enableSecond && !value.seconds) {
|
||||
// @ts-ignore
|
||||
delete value.seconds;
|
||||
} else if (this.enableSecond && value.seconds > 59) {
|
||||
value.minutes = (value.minutes ?? 0) + Math.floor(value.seconds / 60);
|
||||
value.seconds %= 60;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -317,7 +317,7 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
ha-button {
|
||||
--mdc-button-outline-color: var(--primary-color);
|
||||
--mdc-icon-button-size: 24px;
|
||||
--ha-icon-button-size: 24px;
|
||||
}
|
||||
mwc-linear-progress {
|
||||
width: 100%;
|
||||
|
||||
@@ -26,12 +26,12 @@ import { showCategoryRegistryDetailDialog } from "../panels/config/category/show
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-list";
|
||||
import "./ha-list-item";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
|
||||
@customElement("ha-filter-categories")
|
||||
export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
@@ -317,7 +317,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
--mdc-list-item-meta-size: auto;
|
||||
--mdc-list-side-padding-right: var(--ha-space-1);
|
||||
--mdc-list-side-padding-left: var(--ha-space-4);
|
||||
--mdc-icon-button-size: 36px;
|
||||
--ha-icon-button-size: 36px;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
|
||||
@@ -3,6 +3,10 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
@@ -11,10 +15,6 @@ import type {
|
||||
HaFormStringData,
|
||||
HaFormStringSchema,
|
||||
} from "./types";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
|
||||
const MASKED_FIELDS = ["password", "secret", "token"];
|
||||
|
||||
@@ -148,7 +148,7 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -194,7 +194,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
.image=${this.image}
|
||||
.label=${label}
|
||||
.placeholder=${this.placeholder}
|
||||
.helper=${this.helper}
|
||||
.value=${this.value}
|
||||
.valueRenderer=${this.valueRenderer}
|
||||
.required=${this.required}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { mdiRestore } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
|
||||
import "./ha-icon-button";
|
||||
import { mdiRestore } from "@mdi/js";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { conditionalClamp } from "../common/number/clamp";
|
||||
import type { CardGridSize } from "../panels/lovelace/common/compute-card-grid-size";
|
||||
import { DEFAULT_GRID_SIZE } from "../panels/lovelace/common/compute-card-grid-size";
|
||||
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-grid-size-picker")
|
||||
export class HaGridSizeEditor extends LitElement {
|
||||
@@ -245,7 +245,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
}
|
||||
.reset {
|
||||
grid-area: reset;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--ha-icon-button-size: 36px;
|
||||
}
|
||||
.preview {
|
||||
position: relative;
|
||||
|
||||
@@ -14,6 +14,14 @@ export class HaIconButtonArrowPrev extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiArrowRight : mdiArrowLeft;
|
||||
|
||||
@@ -23,6 +31,10 @@ export class HaIconButtonArrowPrev extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,14 @@ export class HaIconButtonNext extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiChevronLeft : mdiChevronRight;
|
||||
|
||||
@@ -23,6 +31,10 @@ export class HaIconButtonNext extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,14 @@ export class HaIconButtonPrev extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiChevronRight : mdiChevronLeft;
|
||||
|
||||
@@ -23,6 +31,10 @@ export class HaIconButtonPrev extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaIconButton } from "./ha-icon-button";
|
||||
@@ -6,41 +7,48 @@ import { HaIconButton } from "./ha-icon-button";
|
||||
export class HaIconButtonToggle extends HaIconButton {
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
mwc-icon-button {
|
||||
position: relative;
|
||||
transition: color 180ms ease-in-out;
|
||||
}
|
||||
mwc-icon-button::before {
|
||||
opacity: 0;
|
||||
transition: opacity 180ms ease-in-out;
|
||||
background-color: var(--primary-text-color);
|
||||
border-radius: var(--ha-border-radius-2xl);
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:host([border-only]) mwc-icon-button::before {
|
||||
background-color: transparent;
|
||||
border: 2px solid var(--primary-text-color);
|
||||
}
|
||||
:host([selected]) mwc-icon-button {
|
||||
color: var(--primary-background-color);
|
||||
}
|
||||
:host([selected]:not([disabled])) mwc-icon-button::before {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
static styles: CSSResultGroup = [
|
||||
HaIconButton.styles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
ha-button::part(base) {
|
||||
position: relative;
|
||||
transition: color 180ms ease-in-out;
|
||||
}
|
||||
ha-button::part(base)::before {
|
||||
opacity: 0;
|
||||
transition: opacity 180ms ease-in-out;
|
||||
background-color: var(--primary-text-color);
|
||||
border-radius: var(--ha-border-radius-2xl);
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:host([border-only]) ha-button::part(base)::before {
|
||||
background-color: transparent;
|
||||
border: 2px solid var(--primary-text-color);
|
||||
}
|
||||
:host([selected]) ha-button::part(base) {
|
||||
color: var(--primary-background-color);
|
||||
background-color: unset;
|
||||
}
|
||||
:host([selected]:not([disabled])) ha-button::part(base)::before {
|
||||
opacity: 1;
|
||||
}
|
||||
::slotted(*) {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -109,7 +109,7 @@ export class HaIconButtonToolbar extends LitElement {
|
||||
|
||||
.icon-toolbar-button {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-icon-button-size: var(--icon-button-toolbar-button);
|
||||
--ha-icon-button-size: var(--icon-button-toolbar-button);
|
||||
--mdc-icon-size: var(--icon-button-toolbar-icon);
|
||||
/* Ensure button is clickable on iOS */
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import "@material/mwc-icon-button";
|
||||
import type { IconButton } from "@material/mwc-icon-button";
|
||||
import type { TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import "./ha-button";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-icon-button")
|
||||
@@ -19,15 +18,19 @@ export class HaIconButton extends LitElement {
|
||||
// These should always be set as properties, not attributes,
|
||||
// so that only the <button> element gets the attribute
|
||||
@property({ type: String, attribute: "aria-haspopup" })
|
||||
override ariaHasPopup!: IconButton["ariaHasPopup"];
|
||||
ariaHasPopup!: "false" | "true" | "menu" | "listbox" | "tree" | "grid";
|
||||
|
||||
@property({ attribute: "hide-title", type: Boolean }) hideTitle = false;
|
||||
|
||||
@query("mwc-icon-button", true) private _button?: IconButton;
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
public override focus() {
|
||||
this._button?.focus();
|
||||
}
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
@@ -36,30 +39,68 @@ export class HaIconButton extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<mwc-icon-button
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
variant="neutral"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
title=${ifDefined(this.hideTitle ? undefined : this.label)}
|
||||
aria-haspopup=${ifDefined(this.ariaHasPopup)}
|
||||
.disabled=${this.disabled}
|
||||
.iconTag=${this.path ? "ha-svg-icon" : "span"}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
>
|
||||
${this.path
|
||||
? html`<ha-svg-icon .path=${this.path}></ha-svg-icon>`
|
||||
: html`<slot></slot>`}
|
||||
</mwc-icon-button>
|
||||
: html`<span><slot></slot></span>`}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
outline: none;
|
||||
--ha-button-height: var(--ha-icon-button-size, 48px);
|
||||
}
|
||||
:host([disabled]) {
|
||||
ha-button {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
--wa-form-control-padding-inline: var(
|
||||
--ha-icon-button-padding-inline,
|
||||
--ha-space-2
|
||||
);
|
||||
--wa-color-on-normal: currentColor;
|
||||
--wa-color-fill-quiet: transparent;
|
||||
}
|
||||
ha-button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
border-radius: 50%;
|
||||
background-color: currentColor;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
mwc-icon-button {
|
||||
--mdc-theme-on-primary: currentColor;
|
||||
--mdc-theme-text-disabled-on-light: var(--disabled-text-color);
|
||||
ha-button::part(base) {
|
||||
width: var(--wa-form-control-height);
|
||||
aspect-ratio: 1;
|
||||
outline-offset: -4px;
|
||||
}
|
||||
ha-button::part(label) {
|
||||
display: flex;
|
||||
}
|
||||
:host([selected]) ha-button::after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
:host(:hover:not([disabled])) ha-button::after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
371
src/components/ha-input.ts
Normal file
371
src/components/ha-input.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
import "@home-assistant/webawesome/dist/components/input/input";
|
||||
import type WaInput from "@home-assistant/webawesome/dist/components/input/input";
|
||||
import { mdiClose, mdiEye, mdiEyeOff, mdiInformationOutline } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { withViewTransition } from "../common/util/view-transition";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
|
||||
@customElement("ha-input")
|
||||
export class HaInput extends LitElement {
|
||||
/** The type of input. */
|
||||
@property()
|
||||
public type:
|
||||
| "date"
|
||||
| "datetime-local"
|
||||
| "email"
|
||||
| "number"
|
||||
| "password"
|
||||
| "search"
|
||||
| "tel"
|
||||
| "text"
|
||||
| "time"
|
||||
| "url" = "text";
|
||||
|
||||
/** The current value of the input. */
|
||||
@property()
|
||||
public value: string | null = null;
|
||||
|
||||
/** The input's size. */
|
||||
@property()
|
||||
public size: "small" | "medium" | "large" = "medium";
|
||||
|
||||
/** The input's visual appearance. */
|
||||
@property()
|
||||
public appearance: "filled" | "outlined" | "filled-outlined" = "outlined";
|
||||
|
||||
/** Draws a pill-style input with rounded edges. */
|
||||
@property({ type: Boolean })
|
||||
public pill = false;
|
||||
|
||||
/** The input's label. */
|
||||
@property()
|
||||
public label = "";
|
||||
|
||||
/** The input's hint. */
|
||||
@property()
|
||||
public hint = "";
|
||||
|
||||
/** Adds a clear button when the input is not empty. */
|
||||
@property({ type: Boolean, attribute: "with-clear" })
|
||||
public withClear = false;
|
||||
|
||||
/** Placeholder text to show as a hint when the input is empty. */
|
||||
@property()
|
||||
public placeholder = "";
|
||||
|
||||
/** Makes the input readonly. */
|
||||
@property({ type: Boolean })
|
||||
public readonly = false;
|
||||
|
||||
/** Adds a button to toggle the password's visibility. */
|
||||
@property({ type: Boolean, attribute: "password-toggle" })
|
||||
public passwordToggle = false;
|
||||
|
||||
/** Determines whether or not the password is currently visible. */
|
||||
@property({ type: Boolean, attribute: "password-visible" })
|
||||
public passwordVisible = false;
|
||||
|
||||
/** Hides the browser's built-in increment/decrement spin buttons for number inputs. */
|
||||
@property({ type: Boolean, attribute: "without-spin-buttons" })
|
||||
public withoutSpinButtons = false;
|
||||
|
||||
/** Makes the input a required field. */
|
||||
@property({ type: Boolean })
|
||||
public required = false;
|
||||
|
||||
/** A regular expression pattern to validate input against. */
|
||||
@property()
|
||||
public pattern?: string;
|
||||
|
||||
/** The minimum length of input that will be considered valid. */
|
||||
@property({ type: Number })
|
||||
public minlength?: number;
|
||||
|
||||
/** The maximum length of input that will be considered valid. */
|
||||
@property({ type: Number })
|
||||
public maxlength?: number;
|
||||
|
||||
/** The input's minimum value. Only applies to date and number input types. */
|
||||
@property()
|
||||
public min?: number | string;
|
||||
|
||||
/** The input's maximum value. Only applies to date and number input types. */
|
||||
@property()
|
||||
public max?: number | string;
|
||||
|
||||
/** Specifies the granularity that the value must adhere to. */
|
||||
@property()
|
||||
public step?: number | "any";
|
||||
|
||||
/** Controls whether and how text input is automatically capitalized. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autocapitalize:
|
||||
| "off"
|
||||
| "none"
|
||||
| "on"
|
||||
| "sentences"
|
||||
| "words"
|
||||
| "characters"
|
||||
| "" = "";
|
||||
|
||||
/** Indicates whether the browser's autocorrect feature is on or off. */
|
||||
@property({ type: Boolean })
|
||||
public autocorrect = false;
|
||||
|
||||
/** Specifies what permission the browser has to provide assistance in filling out form field values. */
|
||||
@property()
|
||||
public autocomplete?: string;
|
||||
|
||||
/** Indicates that the input should receive focus on page load. */
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autofocus = false;
|
||||
|
||||
/** Used to customize the label or icon of the Enter key on virtual keyboards. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public enterkeyhint:
|
||||
| "enter"
|
||||
| "done"
|
||||
| "go"
|
||||
| "next"
|
||||
| "previous"
|
||||
| "search"
|
||||
| "send"
|
||||
| "" = "";
|
||||
|
||||
/** Enables spell checking on the input. */
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public spellcheck = true;
|
||||
|
||||
/** Tells the browser what type of data will be entered by the user. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public inputmode:
|
||||
| "none"
|
||||
| "text"
|
||||
| "decimal"
|
||||
| "numeric"
|
||||
| "tel"
|
||||
| "search"
|
||||
| "email"
|
||||
| "url"
|
||||
| "" = "";
|
||||
|
||||
/** The name of the input, submitted as a name/value pair with form data. */
|
||||
@property()
|
||||
public name?: string;
|
||||
|
||||
/** Disables the form control. */
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
/** Custom validation message to show when the input is invalid. */
|
||||
@property({ attribute: "validation-message" })
|
||||
public validationMessage = "";
|
||||
|
||||
/** When true, validates the input on blur instead of on form submit. */
|
||||
@property({ type: Boolean, attribute: "auto-validate" })
|
||||
public autoValidate = false;
|
||||
|
||||
@state()
|
||||
private _invalid = false;
|
||||
|
||||
@query("wa-input")
|
||||
private _input!: WaInput;
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
/** Selects all the text in the input. */
|
||||
public select(): void {
|
||||
this._input?.select();
|
||||
}
|
||||
|
||||
/** Sets the start and end positions of the text selection (0-based). */
|
||||
public setSelectionRange(
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
selectionDirection?: "forward" | "backward" | "none"
|
||||
): void {
|
||||
this._input?.setSelectionRange(
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
selectionDirection
|
||||
);
|
||||
}
|
||||
|
||||
/** Replaces a range of text with a new string. */
|
||||
public setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void {
|
||||
this._input?.setRangeText(replacement, start, end, selectMode);
|
||||
}
|
||||
|
||||
/** Displays the browser picker for an input element. */
|
||||
public showPicker(): void {
|
||||
this._input?.showPicker();
|
||||
}
|
||||
|
||||
/** Increments the value of a numeric input type by the value of the step attribute. */
|
||||
public stepUp(): void {
|
||||
this._input?.stepUp();
|
||||
}
|
||||
|
||||
/** Decrements the value of a numeric input type by the value of the step attribute. */
|
||||
public stepDown(): void {
|
||||
this._input?.stepDown();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<wa-input
|
||||
.type=${this.type}
|
||||
.value=${this.value}
|
||||
.size=${this.size}
|
||||
.appearance=${this.appearance}
|
||||
.hint=${this._invalid ? this.validationMessage : ""}
|
||||
.withClear=${this.withClear}
|
||||
.placeholder=${this.placeholder}
|
||||
.readonly=${this.readonly}
|
||||
.passwordToggle=${this.passwordToggle}
|
||||
.passwordVisible=${this.passwordVisible}
|
||||
.withoutSpinButtons=${this.withoutSpinButtons}
|
||||
.required=${this.required}
|
||||
.pattern=${this.pattern}
|
||||
.minlength=${this.minlength}
|
||||
.maxlength=${this.maxlength}
|
||||
.min=${this.min}
|
||||
.max=${this.max}
|
||||
.step=${this.step}
|
||||
.autocapitalize=${this.autocapitalize || undefined}
|
||||
.autocorrect=${this.autocorrect ? "on" : "off"}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.autofocus=${this.autofocus}
|
||||
.enterkeyhint=${this.enterkeyhint || undefined}
|
||||
.spellcheck=${this.spellcheck}
|
||||
.inputmode=${this.inputmode || undefined}
|
||||
.name=${this.name}
|
||||
.disabled=${this.disabled}
|
||||
class=${this._invalid ? "invalid" : ""}
|
||||
@input=${this._handleInput}
|
||||
@change=${this._handleChange}
|
||||
@blur=${this._handleBlur}
|
||||
>
|
||||
<div class="label" slot="label">
|
||||
<span>
|
||||
<slot name="label">${this.label}</slot>
|
||||
</span>
|
||||
${this.hint
|
||||
? html`<ha-svg-icon
|
||||
.path=${mdiInformationOutline}
|
||||
id="hint"
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="hint">${this.hint}</ha-tooltip> `
|
||||
: nothing}
|
||||
</div>
|
||||
<slot name="start" slot="start"></slot>
|
||||
<slot name="end" slot="end"></slot>
|
||||
<slot name="clear-icon" slot="clear-icon">
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</slot>
|
||||
<slot name="show-password-icon" slot="show-password-icon">
|
||||
<ha-svg-icon .path=${mdiEye}></ha-svg-icon>
|
||||
</slot>
|
||||
<slot name="hide-password-icon" slot="hide-password-icon">
|
||||
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
|
||||
</slot>
|
||||
</wa-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleInput() {
|
||||
this.value = this._input?.value ?? null;
|
||||
if (this._invalid) {
|
||||
this._invalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleChange() {
|
||||
this.value = this._input?.value ?? null;
|
||||
}
|
||||
|
||||
private _handleBlur() {
|
||||
if (this.autoValidate) {
|
||||
withViewTransition(() => {
|
||||
this._invalid = !this._input.checkValidity();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
wa-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
wa-input::part(base):focus-within {
|
||||
outline: none;
|
||||
--wa-form-control-border-color: var(--ha-color-border-primary-normal);
|
||||
}
|
||||
|
||||
wa-input.invalid {
|
||||
--wa-form-control-border-color: var(--ha-color-border-danger-normal);
|
||||
}
|
||||
|
||||
wa-input::part(label) {
|
||||
margin-block-end: 2px;
|
||||
}
|
||||
|
||||
.label {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
gap: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.label span {
|
||||
line-height: 1;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.label ha-svg-icon {
|
||||
color: var(--ha-color-on-disabled-normal);
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
|
||||
wa-input.invalid::part(hint) {
|
||||
margin-block-start: var(--ha-space-1);
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
font-size: var(--ha-font-size-s);
|
||||
margin-inline-start: var(--ha-space-3);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-input": HaInput;
|
||||
}
|
||||
}
|
||||
@@ -191,7 +191,6 @@ export class HaLanguagePicker extends LitElement {
|
||||
static styles = css`
|
||||
ha-generic-picker {
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -84,13 +84,11 @@ export class HaMarkdown extends LitElement {
|
||||
ha-markdown-element > :is(ol, ul) {
|
||||
padding-inline-start: var(--markdown-list-indent, revert);
|
||||
}
|
||||
li {
|
||||
&:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
& > input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
li:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
}
|
||||
li:has(input[type="checkbox"]) > input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
svg {
|
||||
background-color: var(--markdown-svg-background-color, none);
|
||||
@@ -137,10 +135,10 @@ export class HaMarkdown extends LitElement {
|
||||
--markdown-table-border-width: 0;
|
||||
--markdown-table-padding-inline: 0;
|
||||
--markdown-table-padding-block: 0;
|
||||
th,
|
||||
td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
table[role="presentation"] th,
|
||||
table[role="presentation"] td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
table[role="presentation"] td[valign="top"],
|
||||
table[role="presentation"] th[valign="top"] {
|
||||
|
||||
@@ -196,7 +196,7 @@ export class HaPasswordField extends LitElement {
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -796,7 +796,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: var(--ha-space-3);
|
||||
padding-top: var(--ha-space-4);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
ha-combo-box-item {
|
||||
position: relative;
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border-end-end-radius: 0;
|
||||
@@ -184,8 +185,8 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
|
||||
.clear {
|
||||
margin: 0 -8px;
|
||||
--mdc-icon-button-size: 32px;
|
||||
--mdc-icon-size: 20px;
|
||||
--ha-icon-button-size: 32px;
|
||||
--ha-icon-button-padding-inline: var(--ha-space-1);
|
||||
}
|
||||
.arrow {
|
||||
--mdc-icon-size: 20px;
|
||||
|
||||
@@ -5,6 +5,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-picker-field";
|
||||
import type { HaPickerField } from "./ha-picker-field";
|
||||
import "./ha-svg-icon";
|
||||
@@ -75,7 +76,7 @@ export class HaSelect extends LitElement {
|
||||
|
||||
protected override render() {
|
||||
if (this.disabled) {
|
||||
return this._renderField();
|
||||
return html`${this._renderField()}${this._renderHelper()}`;
|
||||
}
|
||||
|
||||
return html`
|
||||
@@ -116,6 +117,7 @@ export class HaSelect extends LitElement {
|
||||
)
|
||||
: html`<slot></slot>`}
|
||||
</ha-dropdown>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -131,7 +133,6 @@ export class HaSelect extends LitElement {
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@clear=${this._clearValue}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.value=${valueLabel}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@@ -144,6 +145,14 @@ export class HaSelect extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _handleSelect(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.item.value;
|
||||
@@ -194,6 +203,11 @@ export class HaSelect extends LitElement {
|
||||
ha-dropdown::part(menu) {
|
||||
min-width: var(--select-menu-width);
|
||||
}
|
||||
|
||||
ha-input-helper-text {
|
||||
display: block;
|
||||
margin: var(--ha-space-2) 0 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
declare global {
|
||||
|
||||
@@ -66,6 +66,7 @@ export class HaTimeDuration extends LitElement {
|
||||
.enableDay=${this.selector.duration?.enable_day}
|
||||
.enableMillisecond=${this.selector.duration?.enable_millisecond}
|
||||
.allowNegative=${this.selector.duration?.allow_negative}
|
||||
.enableSecond=${this.selector.duration?.enable_second ?? true}
|
||||
></ha-duration-input>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export class HaEntitySelector extends LitElement {
|
||||
if (!this.selector.entity?.multiple) {
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.value=${typeof this.value === "string" ? this.value : ""}
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
} from "../../data/media-player";
|
||||
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
import "../ha-alert";
|
||||
import "../ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../ha-form/types";
|
||||
@@ -72,16 +76,7 @@ export class HaMediaSelector extends LitElement {
|
||||
if (thumbnail === oldThumbnail) {
|
||||
return;
|
||||
}
|
||||
if (thumbnail && thumbnail.startsWith("/")) {
|
||||
this._thumbnailUrl = undefined;
|
||||
// Thumbnails served by local API require authentication
|
||||
getSignedPath(this.hass, thumbnail).then((signedPath) => {
|
||||
this._thumbnailUrl = signedPath.path;
|
||||
});
|
||||
} else if (
|
||||
thumbnail &&
|
||||
thumbnail.startsWith("https://brands.home-assistant.io")
|
||||
) {
|
||||
if (thumbnail && isBrandUrl(thumbnail)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
this._thumbnailUrl = brandsUrl({
|
||||
@@ -89,6 +84,12 @@ export class HaMediaSelector extends LitElement {
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
} else if (thumbnail && thumbnail.startsWith("/")) {
|
||||
this._thumbnailUrl = undefined;
|
||||
// Thumbnails served by local API require authentication
|
||||
getSignedPath(this.hass, thumbnail).then((signedPath) => {
|
||||
this._thumbnailUrl = signedPath.path;
|
||||
});
|
||||
} else {
|
||||
this._thumbnailUrl = thumbnail;
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ export class HaSelectSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.getItems=${this._getItems(options)}
|
||||
.value=${this.value as string | undefined}
|
||||
.value=${typeof this.value === "string" ? this.value : undefined}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
allow-custom-value
|
||||
></ha-generic-picker>
|
||||
@@ -231,7 +231,7 @@ export class HaSelectSelector extends LitElement {
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ?? ""}
|
||||
.value=${(this.value as string) ?? ""}
|
||||
.value=${typeof this.value === "string" ? this.value : ""}
|
||||
.helper=${this.helper ?? ""}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
|
||||
@@ -64,6 +64,15 @@ const SELECTOR_SCHEMAS = {
|
||||
name: "enable_millisecond",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "enable_second",
|
||||
default: true,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "allow_negative",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const,
|
||||
entity: [
|
||||
{
|
||||
|
||||
@@ -141,7 +141,7 @@ export class HaTextSelector extends LitElement {
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -144,6 +144,7 @@ export const computePanels = memoizeOne(
|
||||
if (
|
||||
!isDefaultPanel &&
|
||||
(!panel.title ||
|
||||
panel.show_in_sidebar === false ||
|
||||
hiddenPanels.includes(panel.url_path) ||
|
||||
(panel.default_visible === false &&
|
||||
!panelsOrder.includes(panel.url_path)))
|
||||
|
||||
204
src/components/ha-vacuum-segment-area-mapper.ts
Normal file
204
src/components/ha-vacuum-segment-area-mapper.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { Segment } from "../data/vacuum";
|
||||
import { getVacuumSegments } from "../data/vacuum";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import "./ha-area-picker";
|
||||
import "./ha-md-list";
|
||||
import "./ha-md-list-item";
|
||||
|
||||
type AreaSegmentMapping = Record<string, string[]>; // area ID -> segment IDs
|
||||
|
||||
@customElement("ha-vacuum-segment-area-mapper")
|
||||
export class HaVacuumSegmentAreaMapper extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "entity-id" }) public entityId!: string;
|
||||
|
||||
@property({ attribute: false }) public value?: AreaSegmentMapping;
|
||||
|
||||
@state() private _segments?: Segment[];
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
public get lastSeenSegments() {
|
||||
return this._segments;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
this._loadSegments();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadSegments() {
|
||||
this._loading = true;
|
||||
this._error = undefined;
|
||||
|
||||
try {
|
||||
const result = await getVacuumSegments(this.hass, this.entityId);
|
||||
this._segments = result.segments;
|
||||
} catch (err: any) {
|
||||
this._error = err.message || "Failed to load segments";
|
||||
this._segments = undefined;
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._loading) {
|
||||
return html`
|
||||
<div class="loading">${this.hass.localize("ui.common.loading")}...</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this._error) {
|
||||
return html` <ha-alert alert-type="error">${this._error}</ha-alert> `;
|
||||
}
|
||||
|
||||
if (!this._segments || this._segments.length === 0) {
|
||||
return html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize("ui.dialogs.vacuum_segment_mapping.no_segments")}
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
|
||||
// Group segments by group (if available)
|
||||
const groupedSegments = this._groupSegments(this._segments);
|
||||
|
||||
return html`
|
||||
${Object.entries(groupedSegments).map(
|
||||
([groupName, segments]) => html`
|
||||
${groupName ? html`<h2>${groupName}</h2>` : nothing}
|
||||
<ha-md-list>
|
||||
${segments.map((segment) => this._renderSegment(segment))}
|
||||
</ha-md-list>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _groupSegments(segments: Segment[]): Record<string, Segment[]> {
|
||||
const grouped: Record<string, Segment[]> = {};
|
||||
|
||||
for (const segment of segments) {
|
||||
const group = segment.group || "";
|
||||
if (!grouped[group]) {
|
||||
grouped[group] = [];
|
||||
}
|
||||
grouped[group].push(segment);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
private _renderSegment(segment: Segment) {
|
||||
const mappedAreas = this._getSegmentAreas(segment.id);
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">${segment.name}</span>
|
||||
<ha-area-picker
|
||||
slot="end"
|
||||
.hass=${this.hass}
|
||||
.value=${mappedAreas}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.vacuum_segment_mapping.area_label"
|
||||
)}
|
||||
@value-changed=${this._handleAreaChanged}
|
||||
data-segment-id=${segment.id}
|
||||
></ha-area-picker>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAreaChanged = (ev: CustomEvent) => {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
const segmentId = target.dataset.segmentId;
|
||||
if (segmentId) {
|
||||
this._areaChanged(segmentId, ev);
|
||||
}
|
||||
};
|
||||
|
||||
private _getSegmentAreas(segmentId: string): string | undefined {
|
||||
if (!this.value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find which area(s) contain this segment
|
||||
for (const [areaId, segmentIds] of Object.entries(this.value)) {
|
||||
if (segmentIds.includes(segmentId)) {
|
||||
return areaId;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _areaChanged(segmentId: string, ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const newAreaId = ev.detail.value as string | undefined;
|
||||
|
||||
// Create a copy of the current mapping
|
||||
const newMapping: AreaSegmentMapping = { ...this.value };
|
||||
|
||||
// Remove segment from all areas
|
||||
for (const areaId of Object.keys(newMapping)) {
|
||||
newMapping[areaId] = newMapping[areaId].filter((id) => id !== segmentId);
|
||||
// Remove empty area entries
|
||||
if (newMapping[areaId].length === 0) {
|
||||
delete newMapping[areaId];
|
||||
}
|
||||
}
|
||||
|
||||
// Add segment to new area if specified
|
||||
if (newAreaId) {
|
||||
if (!newMapping[newAreaId]) {
|
||||
newMapping[newAreaId] = [];
|
||||
}
|
||||
newMapping[newAreaId].push(segmentId);
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: newMapping });
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-area-picker {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
margin-inline-start: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: var(--ha-space-4);
|
||||
text-align: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-vacuum-segment-area-mapper": HaVacuumSegmentAreaMapper;
|
||||
}
|
||||
}
|
||||
@@ -423,31 +423,77 @@ export class HaMap extends ReactiveElement {
|
||||
? baseOpacity! + pointIndex * opacityStep!
|
||||
: undefined;
|
||||
|
||||
const thisPoint = path.points[pointIndex];
|
||||
const nextPoint = path.points[pointIndex + 1];
|
||||
|
||||
// DRAW point
|
||||
this._mapPaths.push(
|
||||
Leaflet.circleMarker(path.points[pointIndex].point, {
|
||||
Leaflet.circleMarker(thisPoint.point, {
|
||||
radius: isTouch ? 8 : 3,
|
||||
color: path.color || darkPrimaryColor,
|
||||
opacity,
|
||||
fillOpacity: opacity,
|
||||
interactive: true,
|
||||
}).bindTooltip(
|
||||
this._computePathTooltip(path, path.points[pointIndex]),
|
||||
{ direction: "top" }
|
||||
)
|
||||
}).bindTooltip(this._computePathTooltip(path, thisPoint), {
|
||||
direction: "top",
|
||||
})
|
||||
);
|
||||
|
||||
// DRAW line between this and next point
|
||||
this._mapPaths.push(
|
||||
Leaflet.polyline(
|
||||
[path.points[pointIndex].point, path.points[pointIndex + 1].point],
|
||||
{
|
||||
if (Math.abs(thisPoint.point[1] - nextPoint.point[1]) <= 180) {
|
||||
// if the path does not cross the antimeridian, draw a simple line
|
||||
// between the two points
|
||||
this._mapPaths.push(
|
||||
Leaflet.polyline([thisPoint.point, nextPoint.point], {
|
||||
color: path.color || darkPrimaryColor,
|
||||
opacity,
|
||||
interactive: false,
|
||||
}
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// if the path crosses the antimeridian, split the line into two, to
|
||||
// avoid it being drawn across the entire map
|
||||
const longitudeDifference =
|
||||
((nextPoint.point[1] - thisPoint.point[1] + 540) % 360) - 180;
|
||||
let intersectionLatitude: number;
|
||||
if (longitudeDifference === 0) {
|
||||
// very, very unlikely edge case
|
||||
intersectionLatitude =
|
||||
(thisPoint.point[0] + nextPoint.point[0]) / 2;
|
||||
} else {
|
||||
intersectionLatitude =
|
||||
thisPoint.point[0] +
|
||||
((nextPoint.point[0] - thisPoint.point[0]) *
|
||||
(thisPoint.point[1] > 0
|
||||
? 180 - thisPoint.point[1]
|
||||
: -180 - thisPoint.point[1])) /
|
||||
longitudeDifference;
|
||||
}
|
||||
|
||||
const intersectionPoint1: LatLngTuple = [
|
||||
intersectionLatitude,
|
||||
thisPoint.point[1] > 0 ? 180 : -180,
|
||||
];
|
||||
const intersectionPoint2: LatLngTuple = [
|
||||
intersectionLatitude,
|
||||
nextPoint.point[1] > 0 ? 180 : -180,
|
||||
];
|
||||
|
||||
this._mapPaths.push(
|
||||
Leaflet.polyline([thisPoint.point, intersectionPoint1], {
|
||||
color: path.color || darkPrimaryColor,
|
||||
opacity,
|
||||
interactive: false,
|
||||
})
|
||||
);
|
||||
this._mapPaths.push(
|
||||
Leaflet.polyline([intersectionPoint2, nextPoint.point], {
|
||||
color: path.color || darkPrimaryColor,
|
||||
opacity,
|
||||
interactive: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
const pointIndex = path.points.length - 1;
|
||||
if (pointIndex >= 0) {
|
||||
|
||||
@@ -242,6 +242,10 @@ class BrowseMediaTTS extends LitElement {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
ha-language-picker {
|
||||
width: 100%;
|
||||
}
|
||||
ha-textarea {
|
||||
width: 100%;
|
||||
@@ -260,7 +264,7 @@ class BrowseMediaTTS extends LitElement {
|
||||
}
|
||||
.footer {
|
||||
--mdc-icon-size: 14px;
|
||||
--mdc-icon-button-size: 24px;
|
||||
--ha-icon-button-size: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
} from "../../data/media_source";
|
||||
import { isTTSMediaSource } from "../../data/tts";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import { haStyle, haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
@@ -584,7 +584,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
})}
|
||||
.items=${children}
|
||||
.renderItem=${this._renderGridItem}
|
||||
class="children ${classMap({
|
||||
class="children ha-scrollbar ${classMap({
|
||||
portrait:
|
||||
childrenMediaClass.thumbnail_ratio ===
|
||||
"portrait",
|
||||
@@ -612,6 +612,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
style=${styleMap({
|
||||
height: `${children.length * 72 + 26}px`,
|
||||
})}
|
||||
class="ha-scrollbar"
|
||||
.renderItem=${this._renderListItem}
|
||||
></lit-virtualizer>
|
||||
${currentItem.not_shown
|
||||
@@ -764,6 +765,16 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
return brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
|
||||
if (thumbnailUrl.startsWith("/")) {
|
||||
// Thumbnails served by local API require authentication
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -786,16 +797,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
@@ -979,6 +980,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
@@ -1232,7 +1234,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
.child .play:not(.can_expand) {
|
||||
--mdc-icon-button-size: 70px;
|
||||
--ha-icon-button-size: 70px;
|
||||
--mdc-icon-size: 48px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
@@ -1293,7 +1295,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
transition: all 0.5s;
|
||||
background-color: rgba(var(--rgb-card-background-color), 0.5);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
--mdc-icon-button-size: 40px;
|
||||
--ha-icon-button-size: 40px;
|
||||
}
|
||||
|
||||
ha-list-item:hover .graphic .play {
|
||||
|
||||
@@ -93,8 +93,8 @@ class SearchInputOutlined extends LitElement {
|
||||
}
|
||||
ha-svg-icon,
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
height: var(--mdc-icon-button-size);
|
||||
--ha-icon-button-size: 24px;
|
||||
height: var(--ha-icon-button-size);
|
||||
display: flex;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
@@ -641,7 +641,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
z-index: 1;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 32px;
|
||||
--ha-icon-button-size: 32px;
|
||||
}
|
||||
.summary {
|
||||
display: flex;
|
||||
|
||||
@@ -247,7 +247,7 @@ export class HaTargetPickerValueChip extends LitElement {
|
||||
cursor: default;
|
||||
}
|
||||
.mdc-chip ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--ha-icon-button-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
outline: none;
|
||||
|
||||
@@ -159,6 +159,9 @@ export interface GasSourceTypeEnergyPreference {
|
||||
// kWh/volume meter
|
||||
stat_energy_from: string;
|
||||
|
||||
// Flow rate (m³/h, L/min, etc.)
|
||||
stat_rate?: string;
|
||||
|
||||
// $ meter
|
||||
stat_cost: string | null;
|
||||
|
||||
@@ -174,6 +177,9 @@ export interface WaterSourceTypeEnergyPreference {
|
||||
// volume meter
|
||||
stat_energy_from: string;
|
||||
|
||||
// Flow rate (L/min, gal/min, m³/h, etc.)
|
||||
stat_rate?: string;
|
||||
|
||||
// $ meter
|
||||
stat_cost: string | null;
|
||||
|
||||
@@ -368,6 +374,9 @@ export const getReferencedStatisticIdsPower = (
|
||||
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "gas" || source.type === "water") {
|
||||
if (source.stat_rate) {
|
||||
statIDs.push(source.stat_rate);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -389,6 +398,7 @@ export const getReferencedStatisticIdsPower = (
|
||||
}
|
||||
}
|
||||
statIDs.push(...prefs.device_consumption.map((d) => d.stat_rate));
|
||||
statIDs.push(...prefs.device_consumption_water.map((d) => d.stat_rate));
|
||||
|
||||
return statIDs.filter(Boolean) as string[];
|
||||
};
|
||||
@@ -1391,6 +1401,80 @@ export const calculateSolarConsumedGauge = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Conversion factors from each flow rate unit to L/min.
|
||||
* All HA-supported UnitOfVolumeFlowRate values are covered.
|
||||
*
|
||||
* m³/h → 1000/60 = 16.6667 L/min
|
||||
* m³/min → 1000 L/min
|
||||
* m³/s → 60000 L/min
|
||||
* ft³/min→ 28.3168 L/min
|
||||
* L/h → 1/60 L/min
|
||||
* L/min → 1 L/min
|
||||
* L/s → 60 L/min
|
||||
* gal/h → 3.78541/60 L/min
|
||||
* gal/min→ 3.78541 L/min
|
||||
* gal/d → 3.78541/1440 L/min
|
||||
* mL/s → 0.06 L/min
|
||||
*/
|
||||
|
||||
/** Exact number of liters in one US gallon */
|
||||
const LITERS_PER_GALLON = 3.785411784;
|
||||
|
||||
const FLOW_RATE_TO_LMIN: Record<string, number> = {
|
||||
"m³/h": 1000 / 60,
|
||||
"m³/min": 1000,
|
||||
"m³/s": 60000,
|
||||
"ft³/min": 28.316846592,
|
||||
"L/h": 1 / 60,
|
||||
"L/min": 1,
|
||||
"L/s": 60,
|
||||
"gal/h": LITERS_PER_GALLON / 60,
|
||||
"gal/min": LITERS_PER_GALLON,
|
||||
"gal/d": LITERS_PER_GALLON / 1440,
|
||||
"mL/s": 60 / 1000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current flow rate from an entity state, converted to L/min.
|
||||
* @returns Flow rate in L/min, or undefined if unavailable/invalid.
|
||||
*/
|
||||
export const getFlowRateFromState = (
|
||||
stateObj?: HassEntity
|
||||
): number | undefined => {
|
||||
if (!stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
const value = parseFloat(stateObj.state);
|
||||
if (isNaN(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const unit = stateObj.attributes.unit_of_measurement;
|
||||
const factor = unit ? FLOW_RATE_TO_LMIN[unit] : undefined;
|
||||
if (factor === undefined) {
|
||||
// Unknown unit – return raw value as-is (best effort)
|
||||
return value;
|
||||
}
|
||||
return value * factor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a flow rate value (in L/min) to a human-readable string using
|
||||
* the preferred unit system: metric → L/min, imperial → gal/min.
|
||||
*/
|
||||
export const formatFlowRateShort = (
|
||||
hassLocale: HomeAssistant["locale"],
|
||||
lengthUnitSystem: string,
|
||||
litersPerMin: number
|
||||
): string => {
|
||||
const isMetric = lengthUnitSystem === "km";
|
||||
if (isMetric) {
|
||||
return `${formatNumber(litersPerMin, hassLocale, { maximumFractionDigits: 1 })} L/min`;
|
||||
}
|
||||
const galPerMin = litersPerMin / LITERS_PER_GALLON;
|
||||
return `${formatNumber(galPerMin, hassLocale, { maximumFractionDigits: 1 })} gal/min`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current power value from entity state, normalized to watts (W)
|
||||
* @param stateObj - The entity state object to get power value from
|
||||
|
||||
@@ -3,7 +3,6 @@ import { formatDurationDigital } from "../../common/datetime/format_duration";
|
||||
import type { FrontendLocaleData } from "../translation";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
|
||||
// These attributes are hidden from the more-info window for all entities.
|
||||
export const STATE_ATTRIBUTES = [
|
||||
"entity_id",
|
||||
"assumed_state",
|
||||
@@ -29,8 +28,6 @@ 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"],
|
||||
|
||||
@@ -9,6 +9,7 @@ import { debounce } from "../../common/util/debounce";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LightColor } from "../light";
|
||||
import type { RegistryEntry } from "../registry";
|
||||
import type { Segment } from "../vacuum";
|
||||
|
||||
type EntityCategory = "config" | "diagnostic";
|
||||
|
||||
@@ -120,6 +121,11 @@ export interface SwitchAsXEntityOptions {
|
||||
invert: boolean;
|
||||
}
|
||||
|
||||
export interface VacuumEntityOptions {
|
||||
area_mapping?: Record<string, string[]>;
|
||||
last_seen_segments?: Segment[];
|
||||
}
|
||||
|
||||
export interface EntityRegistryOptions {
|
||||
number?: NumberEntityOptions;
|
||||
sensor?: SensorEntityOptions;
|
||||
@@ -128,6 +134,7 @@ export interface EntityRegistryOptions {
|
||||
lock?: LockEntityOptions;
|
||||
weather?: WeatherEntityOptions;
|
||||
light?: LightEntityOptions;
|
||||
vacuum?: VacuumEntityOptions;
|
||||
switch_as_x?: SwitchAsXEntityOptions;
|
||||
conversation?: Record<string, unknown>;
|
||||
"cloud.alexa"?: Record<string, unknown>;
|
||||
@@ -150,7 +157,8 @@ export interface EntityRegistryEntryUpdateParams {
|
||||
| AlarmControlPanelEntityOptions
|
||||
| CalendarEntityOptions
|
||||
| WeatherEntityOptions
|
||||
| LightEntityOptions;
|
||||
| LightEntityOptions
|
||||
| VacuumEntityOptions;
|
||||
aliases?: string[];
|
||||
labels?: string[];
|
||||
categories?: Record<string, string | null>;
|
||||
|
||||
@@ -37,6 +37,11 @@ export interface LovelaceViewHeaderConfig {
|
||||
badges_wrap?: "wrap" | "scroll";
|
||||
}
|
||||
|
||||
export interface LovelaceViewFooterConfig {
|
||||
card?: LovelaceCardConfig;
|
||||
column_span?: number;
|
||||
}
|
||||
|
||||
export interface LovelaceViewSidebarConfig {
|
||||
sections?: LovelaceSectionConfig[];
|
||||
content_label?: string;
|
||||
@@ -68,6 +73,7 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
|
||||
cards?: LovelaceCardConfig[];
|
||||
sections?: LovelaceSectionRawConfig[];
|
||||
header?: LovelaceViewHeaderConfig;
|
||||
footer?: LovelaceViewFooterConfig;
|
||||
// Only used for section view, it should move to a section view config type when the views will have dedicated editor.
|
||||
sidebar?: LovelaceViewSidebarConfig;
|
||||
}
|
||||
|
||||
@@ -8,12 +8,17 @@ import {
|
||||
mdiPlayBoxMultiple,
|
||||
mdiTooltipAccount,
|
||||
} from "@mdi/js";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
|
||||
export const HOME_PANEL = "home";
|
||||
export const NOT_FOUND_PANEL = "notfound";
|
||||
export const PROFILE_PANEL = "profile";
|
||||
export const LOVELACE_PANEL = "lovelace";
|
||||
|
||||
/** Panel to show when no panel is picked. */
|
||||
export const DEFAULT_PANEL = "home";
|
||||
export const DEFAULT_PANEL = HOME_PANEL;
|
||||
|
||||
export const hasLegacyOverviewPanel = (hass: HomeAssistant): boolean =>
|
||||
Boolean(hass.panels.lovelace?.config);
|
||||
@@ -30,7 +35,7 @@ export const getDefaultPanelUrlPath = (hass: HomeAssistant): string => {
|
||||
getLegacyDefaultPanelUrlPath() ||
|
||||
DEFAULT_PANEL;
|
||||
// If default panel is lovelace and no old overview exists, fall back to home
|
||||
if (defaultPanel === "lovelace" && !hasLegacyOverviewPanel(hass)) {
|
||||
if (defaultPanel === LOVELACE_PANEL && !hasLegacyOverviewPanel(hass)) {
|
||||
return DEFAULT_PANEL;
|
||||
}
|
||||
return defaultPanel;
|
||||
@@ -39,12 +44,16 @@ export const getDefaultPanelUrlPath = (hass: HomeAssistant): string => {
|
||||
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => {
|
||||
const panel = getDefaultPanelUrlPath(hass);
|
||||
|
||||
return (panel ? hass.panels[panel] : undefined) ?? hass.panels[DEFAULT_PANEL];
|
||||
return (
|
||||
(panel ? hass.panels[panel] : undefined) ??
|
||||
hass.panels[DEFAULT_PANEL] ??
|
||||
hass.panels[NOT_FOUND_PANEL]
|
||||
);
|
||||
};
|
||||
|
||||
export const getPanelNameTranslationKey = (panel: PanelInfo) => {
|
||||
if (panel.url_path === "profile") {
|
||||
return "panel.profile" as const;
|
||||
if ([PROFILE_PANEL, NOT_FOUND_PANEL].includes(panel.url_path)) {
|
||||
return `panel.${panel.url_path}` as const;
|
||||
}
|
||||
|
||||
return `panel.${panel.title}` as const;
|
||||
@@ -137,4 +146,22 @@ export const PANEL_ICON_PATHS = {
|
||||
export const getPanelIconPath = (panel: PanelInfo): string | undefined =>
|
||||
PANEL_ICON_PATHS[panel.url_path];
|
||||
|
||||
export const FIXED_PANELS = ["profile", "config"];
|
||||
export const FIXED_PANELS = [PROFILE_PANEL, "config", NOT_FOUND_PANEL];
|
||||
|
||||
export interface PanelMutableParams {
|
||||
title?: string | null;
|
||||
icon?: string | null;
|
||||
require_admin?: boolean | null;
|
||||
show_in_sidebar?: boolean | null;
|
||||
}
|
||||
|
||||
export const updatePanel = (
|
||||
hass: HomeAssistant,
|
||||
urlPath: string,
|
||||
updates: PanelMutableParams
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "frontend/update_panel",
|
||||
url_path: urlPath,
|
||||
...updates,
|
||||
});
|
||||
|
||||
@@ -231,6 +231,7 @@ export interface DurationSelector {
|
||||
enable_day?: boolean;
|
||||
enable_millisecond?: boolean;
|
||||
allow_negative?: boolean;
|
||||
enable_second?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE } from "./entity/entity";
|
||||
|
||||
export type VacuumEntityState =
|
||||
@@ -29,6 +30,7 @@ export const enum VacuumEntityFeature {
|
||||
MAP = 2048,
|
||||
STATE = 4096,
|
||||
START = 8192,
|
||||
CLEAN_AREA = 16384,
|
||||
}
|
||||
|
||||
interface VacuumEntityAttributes extends HassEntityAttributeBase {
|
||||
@@ -62,3 +64,18 @@ export function canReturnHome(stateObj: VacuumEntity): boolean {
|
||||
}
|
||||
return stateObj.state !== "returning";
|
||||
}
|
||||
|
||||
export interface Segment {
|
||||
id: string;
|
||||
name: string;
|
||||
group?: string;
|
||||
}
|
||||
|
||||
export const getVacuumSegments = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string
|
||||
): Promise<{ segments: Segment[] }> =>
|
||||
hass.callWS({
|
||||
type: "vacuum/get_segments",
|
||||
entity_id,
|
||||
});
|
||||
|
||||
@@ -468,13 +468,13 @@ class LightRgbColorPicker extends LitElement {
|
||||
border: none;
|
||||
outline: none;
|
||||
display: block;
|
||||
width: var(--mdc-icon-button-size, 48px);
|
||||
height: var(--mdc-icon-button-size, 48px);
|
||||
width: var(--ha-icon-button-size, 48px);
|
||||
height: var(--ha-icon-button-size, 48px);
|
||||
padding: calc(
|
||||
(var(--mdc-icon-button-size, 48px) - var(--mdc-icon-size, 24px)) / 2
|
||||
(var(--ha-icon-button-size, 48px) - var(--mdc-icon-size, 24px)) / 2
|
||||
);
|
||||
background-color: transparent;
|
||||
border-radius: calc(var(--mdc-icon-button-size, 48px) / 2);
|
||||
border-radius: calc(var(--ha-icon-button-size, 48px) / 2);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: background-color 180ms ease-in-out;
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-vacuum-segment-area-mapper";
|
||||
import type { HaVacuumSegmentAreaMapper } from "../../../../components/ha-vacuum-segment-area-mapper";
|
||||
import type {
|
||||
ExtEntityRegistryEntry,
|
||||
VacuumEntityOptions,
|
||||
} from "../../../../data/entity/entity_registry";
|
||||
import {
|
||||
getExtendedEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
@customElement("ha-more-info-view-vacuum-segment-mapping")
|
||||
export class HaMoreInfoViewVacuumSegmentMapping extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public params!: { entityId: string };
|
||||
|
||||
@state() private _areaMapping?: Record<string, string[]>;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _entry?: ExtEntityRegistryEntry;
|
||||
|
||||
protected firstUpdated() {
|
||||
this._loadCurrentMapping();
|
||||
}
|
||||
|
||||
private async _loadCurrentMapping() {
|
||||
if (!this.params.entityId) return;
|
||||
|
||||
this._entry = await getExtendedEntityRegistryEntry(
|
||||
this.hass,
|
||||
this.params.entityId
|
||||
);
|
||||
|
||||
if (this._entry?.options?.vacuum) {
|
||||
this._areaMapping = this._entry.options.vacuum.area_mapping || {};
|
||||
} else {
|
||||
this._areaMapping = {};
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._areaMapping = ev.detail.value;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
if (!this.params.entityId || !this._areaMapping) return;
|
||||
this._error = undefined;
|
||||
this._submitting = true;
|
||||
|
||||
// Get current segments from the mapper component
|
||||
const mapper = this.shadowRoot!.querySelector(
|
||||
"ha-vacuum-segment-area-mapper"
|
||||
) as HaVacuumSegmentAreaMapper;
|
||||
|
||||
const options: VacuumEntityOptions = {
|
||||
...(this._entry?.options?.vacuum ?? {}),
|
||||
area_mapping: this._areaMapping,
|
||||
last_seen_segments: mapper.lastSeenSegments,
|
||||
};
|
||||
|
||||
try {
|
||||
await updateEntityRegistryEntry(this.hass, this.params.entityId, {
|
||||
options_domain: "vacuum",
|
||||
options: options,
|
||||
});
|
||||
this._dirty = false;
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._areaMapping) {
|
||||
return html`<ha-spinner active></ha-spinner>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
|
||||
<ha-vacuum-segment-area-mapper
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.params.entityId}
|
||||
.value=${this._areaMapping}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-vacuum-segment-area-mapper>
|
||||
|
||||
<div class="footer">
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._dirty || this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ha-spinner {
|
||||
margin: var(--ha-space-8);
|
||||
display: flex;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
ha-vacuum-segment-area-mapper {
|
||||
flex: 1;
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: var(--ha-space-4);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-view-vacuum-segment-mapping": HaMoreInfoViewVacuumSegmentMapping;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
|
||||
export const loadVacuumSegmentMappingView = () =>
|
||||
import("./ha-more-info-view-vacuum-segment-mapping");
|
||||
|
||||
export const showVacuumSegmentMappingView = (
|
||||
element: HTMLElement,
|
||||
localize: LocalizeFunc,
|
||||
entityId: string
|
||||
): void => {
|
||||
fireEvent(element, "show-child-view", {
|
||||
viewTag: "ha-more-info-view-vacuum-segment-mapping",
|
||||
viewImport: loadVacuumSegmentMappingView,
|
||||
viewTitle: localize("ui.dialogs.vacuum_segment_mapping.title"),
|
||||
viewParams: { entityId },
|
||||
});
|
||||
};
|
||||
@@ -35,6 +35,7 @@ import type { HaSlider } from "../../../components/ha-slider";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { showJoinMediaPlayersDialog } from "../../../components/media-player/show-join-media-players-dialog";
|
||||
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { isUnavailableState } from "../../../data/entity/entity";
|
||||
import type {
|
||||
MediaPickedEvent,
|
||||
@@ -169,10 +170,12 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
: nothing}
|
||||
<div
|
||||
class="volume-slider-container"
|
||||
@touchstart=${this._volumeController.handleTouchStart}
|
||||
@touchstart=${this._handleVolumePointerDown}
|
||||
@touchmove=${this._volumeController.handleTouchMove}
|
||||
@touchend=${this._volumeController.handleTouchEnd}
|
||||
@touchcancel=${this._volumeController.handleTouchCancel}
|
||||
@touchend=${this._handleVolumePointerUp}
|
||||
@touchcancel=${this._handleVolumePointerUp}
|
||||
@pointerdown=${this._handleVolumePointerDown}
|
||||
@pointerup=${this._handleVolumePointerUp}
|
||||
@wheel=${this._volumeController.handleWheel}
|
||||
>
|
||||
<ha-slider
|
||||
@@ -182,8 +185,8 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
.value=${Number(this.stateObj.attributes.volume_level) *
|
||||
100}
|
||||
.step=${this._volumeStep}
|
||||
@input=${this._volumeController.handleInput}
|
||||
@change=${this._volumeController.handleChange}
|
||||
@input=${this._handleVolumeInput}
|
||||
@change=${this._handleVolumeChange}
|
||||
></ha-slider>
|
||||
</div>
|
||||
`
|
||||
@@ -596,7 +599,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
}
|
||||
|
||||
.volume ha-icon-button {
|
||||
--mdc-icon-button-size: 32px;
|
||||
--ha-icon-button-size: 32px;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
|
||||
@@ -786,6 +789,36 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
seek_position: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleVolumePointerDown = (
|
||||
ev: TouchEvent | PointerEvent | MouseEvent
|
||||
) => {
|
||||
if (ev.type === "touchstart") {
|
||||
this._volumeController.handleTouchStart(ev as TouchEvent);
|
||||
}
|
||||
if (!this._volumeController.isInteracting) {
|
||||
fireEvent(this, "slider-interaction-start");
|
||||
}
|
||||
};
|
||||
|
||||
private _handleVolumePointerUp = (
|
||||
ev: TouchEvent | PointerEvent | MouseEvent
|
||||
) => {
|
||||
if (ev.type === "touchend" || ev.type === "touchcancel") {
|
||||
this._volumeController.handleTouchEnd(ev as TouchEvent);
|
||||
}
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "slider-interaction-stop");
|
||||
}, 100);
|
||||
};
|
||||
|
||||
private _handleVolumeInput = (ev: Event) => {
|
||||
this._volumeController.handleInput(ev);
|
||||
};
|
||||
|
||||
private _handleVolumeChange = (ev: Event) => {
|
||||
this._volumeController.handleChange(ev);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
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 "../../components/ha-attribute-value";
|
||||
import "../../components/ha-card";
|
||||
import { computeShownAttributes } from "../../data/entity/entity_attributes";
|
||||
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface AttributesViewParams {
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
@customElement("ha-more-info-attributes")
|
||||
class HaMoreInfoAttributes extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry | null;
|
||||
|
||||
@property({ attribute: false }) public params?: AttributesViewParams;
|
||||
|
||||
@state() private _stateObj?: HassEntity;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("params") || changedProps.has("hass")) {
|
||||
if (this.params?.entityId && this.hass) {
|
||||
this._stateObj = this.hass.states[this.params.entityId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params || !this._stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const attributes = computeShownAttributes(this._stateObj);
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
${attributes.map(
|
||||
(attribute) => html`
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this._stateObj!,
|
||||
this.hass.entities,
|
||||
attribute
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
<ha-attribute-value
|
||||
.hass=${this.hass}
|
||||
.attribute=${attribute}
|
||||
.stateObj=${this._stateObj}
|
||||
></ha-attribute-value>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
${this._stateObj.attributes.attribution
|
||||
? html`
|
||||
<div class="attribution">
|
||||
${this._stateObj.attributes.attribution}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--ha-space-6);
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-6));
|
||||
}
|
||||
|
||||
ha-card {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: var(--ha-space-2) var(--ha-space-4);
|
||||
}
|
||||
|
||||
.data-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: var(--ha-space-2) 0;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.data-entry:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.data-entry .value {
|
||||
max-width: 60%;
|
||||
overflow-wrap: break-word;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.key {
|
||||
flex-grow: 1;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
margin-top: var(--ha-space-4);
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-attributes": HaMoreInfoAttributes;
|
||||
}
|
||||
}
|
||||
189
src/dialogs/more-info/ha-more-info-details.ts
Normal file
189
src/dialogs/more-info/ha-more-info-details.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
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 "../../components/ha-attribute-value";
|
||||
import "../../components/ha-card";
|
||||
import { computeShownAttributes } from "../../data/entity/entity_attributes";
|
||||
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface DetailsViewParams {
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
@customElement("ha-more-info-details")
|
||||
class HaMoreInfoDetails extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry | null;
|
||||
|
||||
@property({ attribute: false }) public params?: DetailsViewParams;
|
||||
|
||||
@state() private _stateObj?: HassEntity;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("params") || changedProps.has("hass")) {
|
||||
if (this.params?.entityId && this.hass) {
|
||||
this._stateObj = this.hass.states[this.params.entityId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params || !this._stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const translatedState = this.hass.formatEntityState(this._stateObj);
|
||||
const detailsAttributes = computeShownAttributes(this._stateObj);
|
||||
const detailsAttributeSet = new Set(detailsAttributes);
|
||||
const builtInAttributes = Object.keys(this._stateObj.attributes).filter(
|
||||
(attribute) => !detailsAttributeSet.has(attribute)
|
||||
);
|
||||
const allAttributes = [...detailsAttributes, ...builtInAttributes];
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<section class="section">
|
||||
<h2 class="section-title">
|
||||
${this.hass.localize(
|
||||
"ui.components.entity.entity-state-picker.state"
|
||||
)}
|
||||
</h2>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="attribute-group">
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.translated"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">${translatedState}</div>
|
||||
</div>
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.raw")}
|
||||
</div>
|
||||
<div class="value">${this._stateObj.state}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2 class="section-title">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.attributes")}
|
||||
</h2>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="attribute-group">
|
||||
${this._renderAttributes(allAttributes)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderAttributes(attributes: string[]) {
|
||||
if (attributes.length === 0) {
|
||||
return html`<div class="empty">
|
||||
${this.hass.localize("ui.common.none")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return attributes.map(
|
||||
(attribute) => html`
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this._stateObj!,
|
||||
this.hass.entities,
|
||||
attribute
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
<ha-attribute-value
|
||||
.hass=${this.hass}
|
||||
.attribute=${attribute}
|
||||
.stateObj=${this._stateObj}
|
||||
></ha-attribute-value>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--ha-space-6);
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-6));
|
||||
}
|
||||
|
||||
.section + .section {
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: var(--ha-space-2) var(--ha-space-4);
|
||||
}
|
||||
|
||||
.data-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: var(--ha-space-2) 0;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.attribute-group .data-entry:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.data-entry .value {
|
||||
max-width: 60%;
|
||||
overflow-wrap: break-word;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.key {
|
||||
flex-grow: 1;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
padding: var(--ha-space-2) 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-details": HaMoreInfoDetails;
|
||||
}
|
||||
}
|
||||
@@ -37,15 +37,13 @@ import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-reques
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { withViewTransition } from "../../common/util/view-transition";
|
||||
import "../../components/ha-dialog-header";
|
||||
import "../../components/ha-adaptive-dialog";
|
||||
import "../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
|
||||
import "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-icon-button-prev";
|
||||
import "../../components/ha-related-items";
|
||||
import { computeShownAttributes } from "../../data/entity/entity_attributes";
|
||||
import "../../components/ha-dialog";
|
||||
import type {
|
||||
EntityRegistryEntry,
|
||||
ExtEntityRegistryEntry,
|
||||
@@ -345,31 +343,21 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
case "info":
|
||||
this._resetInitialView();
|
||||
break;
|
||||
case "attributes":
|
||||
this._showAttributes();
|
||||
case "details":
|
||||
this._showDetails();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _showAttributes(): void {
|
||||
import("./ha-more-info-attributes");
|
||||
private _showDetails(): void {
|
||||
import("./ha-more-info-details");
|
||||
this._childView = {
|
||||
viewTag: "ha-more-info-attributes",
|
||||
viewTag: "ha-more-info-details",
|
||||
viewTitle: this.hass.localize("ui.dialogs.more_info_control.details"),
|
||||
viewParams: { entityId: this._entityId },
|
||||
};
|
||||
}
|
||||
|
||||
private _hasDisplayableAttributes(): boolean {
|
||||
if (!this._entityId) {
|
||||
return false;
|
||||
}
|
||||
const stateObj = this.hass.states[this._entityId];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return computeShownAttributes(stateObj).length > 0;
|
||||
}
|
||||
|
||||
private _goToAddEntityTo(ev) {
|
||||
// Only check for request-selected events (from menu items), not regular clicks (from icon button)
|
||||
if (ev.type === "request-selected" && !shouldHandleRequestSelectedEvent(ev))
|
||||
@@ -448,7 +436,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
.width=${this._fill ? "full" : this.large ? "large" : "medium"}
|
||||
@@ -457,205 +445,199 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
?prevent-scrim-close=${!this._isEscapeEnabled}
|
||||
flexcontent
|
||||
>
|
||||
<ha-dialog-header slot="header">
|
||||
${showCloseIcon
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
data-dialog="close"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-prev
|
||||
slot="navigationIcon"
|
||||
@click=${this._goBack}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.back_to_info"
|
||||
)}
|
||||
></ha-icon-button-prev>
|
||||
`}
|
||||
<span slot="title" @click=${this._enlarge} class="title">
|
||||
${breadcrumb.length > 0
|
||||
? !__DEMO__ && isAdmin
|
||||
${showCloseIcon
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="headerNavigationIcon"
|
||||
@click=${this.closeDialog}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-prev
|
||||
slot="headerNavigationIcon"
|
||||
@click=${this._goBack}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.back_to_info"
|
||||
)}
|
||||
></ha-icon-button-prev>
|
||||
`}
|
||||
<span slot="headerTitle" @click=${this._enlarge} class="title">
|
||||
${breadcrumb.length > 0
|
||||
? !__DEMO__ && isAdmin
|
||||
? html`
|
||||
<button class="breadcrumb" @click=${this._breadcrumbClick}>
|
||||
${breadcrumb.join(isRTL ? " ◂ " : " ▸ ")}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
<p class="breadcrumb">
|
||||
${breadcrumb.join(isRTL ? " ◂ " : " ▸ ")}
|
||||
</p>
|
||||
`
|
||||
: nothing}
|
||||
<p class="main">${title}</p>
|
||||
</span>
|
||||
${isDefaultView
|
||||
? html`
|
||||
${this._shouldShowHistory(domain)
|
||||
? html`
|
||||
<button class="breadcrumb" @click=${this._breadcrumbClick}>
|
||||
${breadcrumb.join(isRTL ? " ◂ " : " ▸ ")}
|
||||
</button>
|
||||
<ha-icon-button
|
||||
slot="headerActionItems"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.history"
|
||||
)}
|
||||
.path=${mdiChartBoxOutline}
|
||||
@click=${this._goToHistory}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<p class="breadcrumb">
|
||||
${breadcrumb.join(isRTL ? " ◂ " : " ▸ ")}
|
||||
</p>
|
||||
: nothing}
|
||||
${!__DEMO__ && isAdmin
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="headerActionItems"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.settings"
|
||||
)}
|
||||
.path=${mdiCogOutline}
|
||||
@click=${this._goToSettings}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown
|
||||
slot="headerActionItems"
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleMenuAction}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${deviceId
|
||||
? html`
|
||||
<ha-dropdown-item value="device">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${deviceType === "service"
|
||||
? mdiTransitConnectionVariant
|
||||
: mdiDevices}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.device_or_service_info",
|
||||
{
|
||||
type: this.hass.localize(
|
||||
`ui.dialogs.more_info_control.device_type.${deviceType}`
|
||||
),
|
||||
}
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${this._shouldShowEditIcon(domain, stateObj)
|
||||
? html`
|
||||
<ha-dropdown-item value="edit">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiPencilOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.edit"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${this._entry &&
|
||||
stateObj &&
|
||||
domain === "light" &&
|
||||
lightSupportsFavoriteColors(stateObj)
|
||||
? html`
|
||||
<ha-dropdown-item value="toggle_edit">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${this._infoEditMode
|
||||
? mdiPencilOff
|
||||
: mdiPencil}
|
||||
></ha-svg-icon>
|
||||
${this._infoEditMode
|
||||
? this.hass.localize(
|
||||
`ui.dialogs.more_info_control.exit_edit_mode`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.dialogs.more_info_control.${domain}.edit_mode`
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-dropdown-item value="related">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.related"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="details">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFormatListBulletedSquare}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.details"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
${this._shouldShowAddEntityTo()
|
||||
? html`
|
||||
<ha-dropdown-item value="add_to">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiPlusBoxMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_entity_to"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing}
|
||||
<p class="main">${title}</p>
|
||||
</span>
|
||||
${isDefaultView
|
||||
? html`
|
||||
${this._shouldShowHistory(domain)
|
||||
: !__DEMO__ && this._shouldShowAddEntityTo()
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="actionItems"
|
||||
slot="headerActionItems"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.history"
|
||||
"ui.dialogs.more_info_control.add_entity_to"
|
||||
)}
|
||||
.path=${mdiChartBoxOutline}
|
||||
@click=${this._goToHistory}
|
||||
.path=${mdiPlusBoxMultipleOutline}
|
||||
@click=${this._goToAddEntityTo}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
${!__DEMO__ && isAdmin
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="actionItems"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.settings"
|
||||
)}
|
||||
.path=${mdiCogOutline}
|
||||
@click=${this._goToSettings}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown
|
||||
slot="actionItems"
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleMenuAction}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: isSpecificInitialView
|
||||
? html`
|
||||
<ha-dropdown
|
||||
slot="headerActionItems"
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleMenuAction}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${deviceId
|
||||
? html`
|
||||
<ha-dropdown-item value="device">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${deviceType === "service"
|
||||
? mdiTransitConnectionVariant
|
||||
: mdiDevices}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.device_or_service_info",
|
||||
{
|
||||
type: this.hass.localize(
|
||||
`ui.dialogs.more_info_control.device_type.${deviceType}`
|
||||
),
|
||||
}
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${this._shouldShowEditIcon(domain, stateObj)
|
||||
? html`
|
||||
<ha-dropdown-item value="edit">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiPencilOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.edit"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${this._entry &&
|
||||
stateObj &&
|
||||
domain === "light" &&
|
||||
lightSupportsFavoriteColors(stateObj)
|
||||
? html`
|
||||
<ha-dropdown-item value="toggle_edit">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${this._infoEditMode
|
||||
? mdiPencilOff
|
||||
: mdiPencil}
|
||||
></ha-svg-icon>
|
||||
${this._infoEditMode
|
||||
? this.hass.localize(
|
||||
`ui.dialogs.more_info_control.exit_edit_mode`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.dialogs.more_info_control.${domain}.edit_mode`
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-dropdown-item value="related">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.related"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
${this._hasDisplayableAttributes()
|
||||
? html`
|
||||
<ha-dropdown-item value="attributes">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFormatListBulletedSquare}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.attributes"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${this._shouldShowAddEntityTo()
|
||||
? html`
|
||||
<ha-dropdown-item value="add_to">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiPlusBoxMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_entity_to"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-dropdown>
|
||||
`
|
||||
: !__DEMO__ && this._shouldShowAddEntityTo()
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="actionItems"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_entity_to"
|
||||
)}
|
||||
.path=${mdiPlusBoxMultipleOutline}
|
||||
@click=${this._goToAddEntityTo}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-dropdown-item value="info">
|
||||
<ha-svg-icon slot="icon" .path=${mdiInformationOutline}>
|
||||
</ha-svg-icon>
|
||||
${this.hass.localize("ui.dialogs.more_info_control.info")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`
|
||||
: isSpecificInitialView
|
||||
? html`
|
||||
<ha-dropdown
|
||||
slot="actionItems"
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleMenuAction}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown-item value="info">
|
||||
<ha-svg-icon slot="icon" .path=${mdiInformationOutline}>
|
||||
</ha-svg-icon>
|
||||
${this.hass.localize("ui.dialogs.more_info_control.info")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
: nothing}
|
||||
<div
|
||||
class=${classMap({
|
||||
"content-wrapper": true,
|
||||
@@ -734,7 +716,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
)}
|
||||
${this.renderScrollableFades()}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
</ha-adaptive-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -795,7 +777,17 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
haStyleDialogFixedTop,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
ha-dialog {
|
||||
:host {
|
||||
--ha-bottom-sheet-height: calc(
|
||||
100vh - max(var(--safe-area-inset-top), 48px)
|
||||
);
|
||||
--ha-bottom-sheet-height: calc(
|
||||
100dvh - max(var(--safe-area-inset-top), 48px)
|
||||
);
|
||||
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
|
||||
}
|
||||
|
||||
ha-adaptive-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,9 +105,11 @@ class MoreInfoContent extends LitElement {
|
||||
if (!stateObj) {
|
||||
return null;
|
||||
}
|
||||
const entityName = entry
|
||||
? computeEntityName(stateObj, hass.entities, hass.devices)
|
||||
: undefined;
|
||||
const entityName = computeEntityName(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices
|
||||
);
|
||||
const { area } = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
|
||||
@@ -177,18 +177,6 @@ export class QuickBar extends LitElement {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
};
|
||||
|
||||
// fallback in case the closed event is not fired
|
||||
private _dialogCloseStarted = () => {
|
||||
setTimeout(
|
||||
() => {
|
||||
if (this._opened) {
|
||||
this._dialogClosed();
|
||||
}
|
||||
},
|
||||
350 // close animation timeout is 300ms
|
||||
);
|
||||
};
|
||||
|
||||
// #endregion lifecycle
|
||||
|
||||
// #region render
|
||||
@@ -246,7 +234,6 @@ export class QuickBar extends LitElement {
|
||||
hideActions
|
||||
@wa-show=${this._showTriggered}
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@wa-hide=${this._dialogCloseStarted}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${!this._loading && this._opened
|
||||
|
||||
@@ -79,7 +79,6 @@ class DialogRestartWait extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
.headerTitle=${this._title}
|
||||
width="medium"
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div class="content">
|
||||
|
||||
@@ -14,6 +14,7 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-assist-chat";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-header";
|
||||
import "../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
|
||||
@@ -21,7 +22,6 @@ import "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-icon-next";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/ha-dialog";
|
||||
import type { AssistPipeline } from "../../data/assist_pipeline";
|
||||
import {
|
||||
getAssistPipeline,
|
||||
@@ -164,17 +164,14 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
: nothing}
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
<a
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.help")}
|
||||
.path=${mdiHelpCircleOutline}
|
||||
href=${documentationUrl(this.hass, "/docs/assist/")}
|
||||
slot="actionItems"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.help")}
|
||||
.path=${mdiHelpCircleOutline}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
></ha-icon-button>
|
||||
</ha-dialog-header>
|
||||
|
||||
${this._errorLoadAssist
|
||||
|
||||
@@ -32,21 +32,28 @@ const initRouting = () => {
|
||||
new CacheFirst({ matchOptions: { ignoreSearch: true } })
|
||||
);
|
||||
|
||||
// Cache any brand images used for 30 days
|
||||
// Use revalidation so cache is always available during an extended outage
|
||||
// Cache any brand images used for 1 day
|
||||
// Brands are proxied via the local API with backend caching.
|
||||
// Strip the rotating access token from cache keys so token rotation
|
||||
// doesn't bust the cache, while preserving other params like "placeholder".
|
||||
registerRoute(
|
||||
({ url, request }) =>
|
||||
url.origin === "https://brands.home-assistant.io" &&
|
||||
url.pathname.startsWith("/api/brands/") &&
|
||||
request.destination === "image",
|
||||
new StaleWhileRevalidate({
|
||||
cacheName: "brands",
|
||||
// CORS must be forced to work for CSS images
|
||||
fetchOptions: { mode: "cors", credentials: "omit" },
|
||||
plugins: [
|
||||
{
|
||||
cacheKeyWillBeUsed: async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
url.searchParams.delete("token");
|
||||
return url.href;
|
||||
},
|
||||
},
|
||||
// Add 404 so we quickly respond to domains with missing images
|
||||
new CacheableResponsePlugin({ statuses: [0, 200, 404] }),
|
||||
new ExpirationPlugin({
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30,
|
||||
maxAgeSeconds: 60 * 60 * 24,
|
||||
purgeOnQuotaError: true,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -39,11 +39,10 @@ class HassSubpage extends LitElement {
|
||||
`
|
||||
: this.backPath
|
||||
? html`
|
||||
<a href=${this.backPath}>
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
></ha-icon-button-arrow-prev>
|
||||
</a>
|
||||
<ha-icon-button-arrow-prev
|
||||
href=${this.backPath}
|
||||
.hass=${this.hass}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
|
||||
@@ -51,8 +51,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public supervisor = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
|
||||
|
||||
@property({ attribute: false }) public initialCollapsedGroups: string[] = [];
|
||||
@@ -322,7 +320,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
.value=${id}
|
||||
.clickAction=${this._handleGroupBy}
|
||||
.selected=${id === this._groupColumn}
|
||||
class=${classMap({ selected: id === this._groupColumn })}
|
||||
>
|
||||
@@ -383,7 +380,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
.route=${this.route}
|
||||
.tabs=${this.tabs}
|
||||
.mainPage=${this.mainPage}
|
||||
.supervisor=${this.supervisor}
|
||||
.pane=${showPane && this.showFilters}
|
||||
@sorting-changed=${this._sortingChanged}
|
||||
>
|
||||
@@ -489,7 +485,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
: ""}
|
||||
<ha-data-table
|
||||
.hass=${this.hass}
|
||||
.localize=${localize}
|
||||
.narrow=${this.narrow}
|
||||
.columns=${this.columns}
|
||||
.data=${this.data}
|
||||
|
||||
@@ -5,7 +5,8 @@ import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import { restoreScroll } from "../common/decorators/restore-scroll";
|
||||
import { goBack } from "../common/navigate";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import { goBack, navigate } from "../common/navigate";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-icon-button-arrow-prev";
|
||||
import "../components/ha-menu-button";
|
||||
@@ -14,6 +15,11 @@ import "../components/ha-tab";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
|
||||
const normalizePathname = (pathname: string): string =>
|
||||
pathname.endsWith("/") && pathname.length > 1
|
||||
? pathname.slice(0, -1)
|
||||
: pathname;
|
||||
|
||||
export interface PageNavigation {
|
||||
path: string;
|
||||
translationKey?: string;
|
||||
@@ -88,9 +94,8 @@ class HassTabsSubpage extends LitElement {
|
||||
|
||||
return shownTabs.map(
|
||||
(page) => html`
|
||||
<a href=${page.path}>
|
||||
<a href=${page.path} @click=${this._tabClicked}>
|
||||
<ha-tab
|
||||
.hass=${this.hass}
|
||||
.active=${page.path === activeTab?.path}
|
||||
.narrow=${this.narrow}
|
||||
.name=${page.translationKey
|
||||
@@ -112,8 +117,9 @@ class HassTabsSubpage extends LitElement {
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("route")) {
|
||||
const currentPath = `${this.route.prefix}${this.route.path}`;
|
||||
this._activeTab = this.tabs.find((tab) =>
|
||||
`${this.route.prefix}${this.route.path}`.includes(tab.path)
|
||||
this._isActiveTabPath(tab.path, currentPath)
|
||||
);
|
||||
}
|
||||
super.willUpdate(changedProperties);
|
||||
@@ -143,11 +149,10 @@ class HassTabsSubpage extends LitElement {
|
||||
`
|
||||
: this.backPath
|
||||
? html`
|
||||
<a href=${this.backPath}>
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
></ha-icon-button-arrow-prev>
|
||||
</a>
|
||||
<ha-icon-button-arrow-prev
|
||||
.href=${this.backPath}
|
||||
.hass=${this.hass}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@@ -210,6 +215,36 @@ class HassTabsSubpage extends LitElement {
|
||||
goBack();
|
||||
}
|
||||
|
||||
private _isActiveTabPath(tabPath: string, currentPath: string): boolean {
|
||||
try {
|
||||
const tabUrl = new URL(tabPath, window.location.origin);
|
||||
const currentUrl = new URL(currentPath, window.location.origin);
|
||||
|
||||
const tabPathname = normalizePathname(tabUrl.pathname);
|
||||
const currentPathname = normalizePathname(currentUrl.pathname);
|
||||
|
||||
if (
|
||||
currentPathname === tabPathname ||
|
||||
currentPathname.startsWith(`${tabPathname}/`)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (_err) {
|
||||
return currentPath === tabPath || currentPath.startsWith(`${tabPath}/`);
|
||||
}
|
||||
}
|
||||
|
||||
private async _tabClicked(ev: MouseEvent): Promise<void> {
|
||||
const href = isNavigationClick(ev);
|
||||
if (!href) {
|
||||
return;
|
||||
}
|
||||
|
||||
await navigate(href, { replace: true });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
|
||||
@@ -35,6 +35,7 @@ const COMPONENTS = {
|
||||
security: () => import("../panels/security/ha-panel-security"),
|
||||
climate: () => import("../panels/climate/ha-panel-climate"),
|
||||
home: () => import("../panels/home/ha-panel-home"),
|
||||
notfound: () => import("../panels/notfound/ha-panel-notfound"),
|
||||
};
|
||||
|
||||
@customElement("partial-panel-resolver")
|
||||
|
||||
@@ -11,8 +11,8 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-list";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-list";
|
||||
import "../components/ha-list-item";
|
||||
import "../components/ha-radio";
|
||||
import "../components/ha-spinner";
|
||||
@@ -486,7 +486,7 @@ class OnboardingLocation extends LitElement {
|
||||
right: 10px;
|
||||
inset-inline-end: 10px;
|
||||
inset-inline-start: initial;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { mdiMenu } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { computeRouteTail } from "../../common/url/route";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import "../../components/ha-icon-button";
|
||||
import type { HassioAddonDetails } from "../../data/hassio/addon";
|
||||
@@ -24,7 +25,6 @@ import {
|
||||
showConfirmationDialog,
|
||||
} from "../../dialogs/generic/show-dialog-box";
|
||||
import "../../layouts/hass-loading-screen";
|
||||
import { computeRouteTail } from "../../common/url/route";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../../types";
|
||||
|
||||
interface AppPanelConfig {
|
||||
@@ -43,7 +43,7 @@ class HaPanelApp extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public panel!: PanelInfo<AppPanelConfig>;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@state() private _addon?: HassioAddonDetails;
|
||||
|
||||
@@ -51,6 +51,8 @@ class HaPanelApp extends LitElement {
|
||||
|
||||
@state() private _kioskMode = false;
|
||||
|
||||
@state() private _iframeLoaded = false;
|
||||
|
||||
private _enabledKioskMode = false;
|
||||
|
||||
private _sessionKeepAlive?: number;
|
||||
@@ -117,7 +119,7 @@ class HaPanelApp extends LitElement {
|
||||
${!this._kioskMode &&
|
||||
(this.narrow || this.hass.dockedSidebar === "always_hidden")
|
||||
? html`
|
||||
<div class="header ${classMap({ narrow: this.narrow })}">
|
||||
<div class="header">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
|
||||
.path=${mdiMenu}
|
||||
@@ -128,6 +130,10 @@ class HaPanelApp extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
<iframe
|
||||
class=${classMap({
|
||||
loaded: this._iframeLoaded,
|
||||
"kiosk-mode": this._kioskMode,
|
||||
})}
|
||||
title=${this._addon.name}
|
||||
src=${this._addon.ingress_url!}
|
||||
@load=${this._checkLoaded}
|
||||
@@ -156,6 +162,7 @@ class HaPanelApp extends LitElement {
|
||||
|
||||
if (addon && addon !== oldAddon) {
|
||||
this._loadingMessage = undefined;
|
||||
this._iframeLoaded = false;
|
||||
// Reset state when switching apps
|
||||
if (this._enabledKioskMode) {
|
||||
fireEvent(window, "hass-kiosk-mode", { enable: false });
|
||||
@@ -320,6 +327,8 @@ class HaPanelApp extends LitElement {
|
||||
|
||||
private async _checkLoaded(ev: Event): Promise<void> {
|
||||
const iframe = ev.target as HTMLIFrameElement;
|
||||
this._iframeLoaded = true;
|
||||
|
||||
if (
|
||||
!this._addon ||
|
||||
iframe.contentDocument?.body.textContent !== "502: Bad Gateway"
|
||||
@@ -352,6 +361,7 @@ class HaPanelApp extends LitElement {
|
||||
|
||||
private async _reloadIframe(): Promise<void> {
|
||||
const addonSlug = this._addon!.slug;
|
||||
this._iframeLoaded = false;
|
||||
this._addon = undefined;
|
||||
await Promise.all([
|
||||
this.updateComplete,
|
||||
@@ -431,12 +441,29 @@ class HaPanelApp extends LitElement {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background-color: var(--primary-background-color);
|
||||
opacity: 0;
|
||||
transition: opacity var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
|
||||
iframe.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.header + iframe {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
:host([narrow]) iframe {
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
height: calc(100% - var(--safe-area-inset-top, 0px));
|
||||
}
|
||||
|
||||
:host([narrow]) .header + iframe {
|
||||
padding-top: 0;
|
||||
height: calc(100% - 40px - var(--safe-area-inset-top, 0px));
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -452,6 +479,11 @@ class HaPanelApp extends LitElement {
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
|
||||
:host([narrow]) .header {
|
||||
height: calc(40px + var(--safe-area-inset-top, 0px));
|
||||
padding-top: var(--safe-area-inset-top, 0);
|
||||
}
|
||||
|
||||
.main-title {
|
||||
margin-inline-start: var(--ha-space-6);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
|
||||
@@ -488,7 +488,7 @@ export class HAFullCalendar extends LitElement {
|
||||
|
||||
.prev,
|
||||
.next {
|
||||
--mdc-icon-button-size: 32px;
|
||||
--ha-icon-button-size: 32px;
|
||||
}
|
||||
|
||||
ha-fab {
|
||||
|
||||
@@ -58,7 +58,8 @@ class PanelClimate extends LitElement {
|
||||
oldHass.entities !== this.hass.entities ||
|
||||
oldHass.devices !== this.hass.devices ||
|
||||
oldHass.areas !== this.hass.areas ||
|
||||
oldHass.floors !== this.hass.floors
|
||||
oldHass.floors !== this.hass.floors ||
|
||||
oldHass.panels !== this.hass.panels
|
||||
) {
|
||||
if (this.hass.config.state === "RUNNING") {
|
||||
this._debounceRegistriesChanged();
|
||||
|
||||
@@ -103,10 +103,12 @@ const processAreasForClimate = (
|
||||
heading_style: "subtitle",
|
||||
type: "heading",
|
||||
heading: area.name,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: `/home/areas-${area.area_id}`,
|
||||
},
|
||||
tap_action: hass.panels.home
|
||||
? {
|
||||
action: "navigate",
|
||||
navigation_path: `/home/areas-${area.area_id}`,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
cards.push(...areaCards);
|
||||
}
|
||||
|
||||
@@ -106,12 +106,11 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
filterable: true,
|
||||
},
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
title: "",
|
||||
label: localize("ui.panel.config.generic.headers.actions"),
|
||||
type: "overflow-menu",
|
||||
showNarrow: true,
|
||||
hideable: false,
|
||||
moveable: false,
|
||||
template: (credential) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -9,10 +9,16 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import type { HassioAddonDetails } from "../../../data/hassio/addon";
|
||||
import { fetchHassioAddonInfo } from "../../../data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import {
|
||||
addStoreRepository,
|
||||
fetchSupervisorStore,
|
||||
} from "../../../data/supervisor/store";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-error-screen";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
@@ -39,6 +45,8 @@ class HaConfigAppDashboard extends LitElement {
|
||||
|
||||
@state() private _fromStore = false;
|
||||
|
||||
@state() private _loading = true;
|
||||
|
||||
private _computeTail = memoizeOne((route: Route) => {
|
||||
const pathParts = route.path.split("/").filter(Boolean);
|
||||
// Path is like /<slug>/info or /<slug>/config
|
||||
@@ -53,8 +61,15 @@ class HaConfigAppDashboard extends LitElement {
|
||||
|
||||
protected async firstUpdated(): Promise<void> {
|
||||
this._fromStore = extractSearchParam("store") === "true";
|
||||
await this._loadAddon();
|
||||
const repositoryUrl = extractSearchParam("repository_url");
|
||||
if (repositoryUrl) {
|
||||
navigate(`/config/app/${this.route.path.split("/")[1]}`, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
await this._loadAddon(repositoryUrl);
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
@@ -63,7 +78,7 @@ class HaConfigAppDashboard extends LitElement {
|
||||
const oldSlug = oldRoute?.path.split("/")[1];
|
||||
const newSlug = this.route.path.split("/")[1];
|
||||
|
||||
if (oldSlug !== newSlug && newSlug) {
|
||||
if (oldSlug !== newSlug && newSlug && !this._loading) {
|
||||
this._loadAddon();
|
||||
}
|
||||
}
|
||||
@@ -138,7 +153,7 @@ class HaConfigAppDashboard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _loadAddon(): Promise<void> {
|
||||
private async _loadAddon(repositoryUrl?: string | null): Promise<void> {
|
||||
const slug = this.route.path.split("/")[1];
|
||||
if (!slug) {
|
||||
this._error = "No addon specified";
|
||||
@@ -148,10 +163,56 @@ class HaConfigAppDashboard extends LitElement {
|
||||
try {
|
||||
this._addon = await fetchHassioAddonInfo(this.hass, slug);
|
||||
} catch (err: any) {
|
||||
this._error = `Error loading addon: ${extractApiErrorMessage(err)}`;
|
||||
if (repositoryUrl) {
|
||||
try {
|
||||
await this._handleMissingRepository(slug, repositoryUrl);
|
||||
if (this._addon) {
|
||||
// Clear error if we successfully added the repository and loaded the addon
|
||||
this._error = undefined;
|
||||
return;
|
||||
}
|
||||
} catch (addRepoErr: any) {
|
||||
this._error = extractApiErrorMessage(addRepoErr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._error = `Error loading app: ${extractApiErrorMessage(err)}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleMissingRepository(
|
||||
slug: string,
|
||||
repositoryUrl: string
|
||||
): Promise<void> {
|
||||
const storeInfo = await fetchSupervisorStore(this.hass);
|
||||
if (storeInfo.repositories.some((repo) => repo.source === repositoryUrl)) {
|
||||
// Repository is already installed, addon just doesn't exist
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.my.add_repository_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.apps.my.add_repository_description",
|
||||
{ repository: repositoryUrl }
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.add"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
}))
|
||||
) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.apps.my.error_repository_not_found"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await addStoreRepository(this.hass, repositoryUrl);
|
||||
this._addon = await fetchHassioAddonInfo(this.hass, slug);
|
||||
}
|
||||
|
||||
private async _apiCalled(ev): Promise<void> {
|
||||
if (!ev.detail.success) {
|
||||
return;
|
||||
|
||||
@@ -2,12 +2,14 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
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 {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../../../../common/translations/localize";
|
||||
import { CONDITION_ICONS } from "../../../../../components/ha-condition-icon";
|
||||
import "../../../../../components/ha-dropdown-item";
|
||||
import type { PickerComboBoxItem } from "../../../../../components/ha-picker-combo-box";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
|
||||
import {
|
||||
DYNAMIC_PREFIX,
|
||||
getValueFromDynamic,
|
||||
@@ -21,8 +23,9 @@ import {
|
||||
getConditionObjectId,
|
||||
subscribeConditions,
|
||||
} from "../../../../../data/condition";
|
||||
import { domainToName } from "../../../../../data/integration";
|
||||
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
|
||||
import "../../condition/ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "../../condition/ha-automation-condition-editor";
|
||||
import "../../condition/types/ha-automation-condition-and";
|
||||
@@ -89,43 +92,24 @@ export class HaConditionAction
|
||||
? `${DYNAMIC_PREFIX}${this.action.condition}`
|
||||
: this.action.condition;
|
||||
|
||||
let valueLabel = value;
|
||||
|
||||
const items = html`${this._processedTypes(
|
||||
this._conditionDescriptions,
|
||||
this.hass.localize
|
||||
).map(([opt, label, condition]) => {
|
||||
const selected = value === opt;
|
||||
|
||||
if (selected) {
|
||||
valueLabel = label;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dropdown-item .value=${opt} .selected=${selected}>
|
||||
<ha-condition-icon
|
||||
.hass=${this.hass}
|
||||
slot="icon"
|
||||
.condition=${condition}
|
||||
></ha-condition-icon>
|
||||
${label}
|
||||
</ha-dropdown-item>
|
||||
`;
|
||||
})}`;
|
||||
|
||||
return html`
|
||||
${this.inSidebar || (!this.inSidebar && !this.indent)
|
||||
? html`
|
||||
<ha-select
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type_select"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
.value=${valueLabel}
|
||||
@selected=${this._typeChanged}
|
||||
>
|
||||
${items}
|
||||
</ha-select>
|
||||
.value=${value}
|
||||
.getItems=${this._processedTypes(
|
||||
this._conditionDescriptions,
|
||||
this.hass.localize
|
||||
)}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._typeChanged}
|
||||
></ha-generic-picker>
|
||||
`
|
||||
: nothing}
|
||||
${(this.indent && buildingBlock) ||
|
||||
@@ -150,36 +134,83 @@ export class HaConditionAction
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueRenderer = (value: string) => {
|
||||
const isDynamicValue = isDynamic(value);
|
||||
const condition = isDynamicValue ? getValueFromDynamic(value) : value;
|
||||
|
||||
let label = condition;
|
||||
|
||||
if (isDynamicValue) {
|
||||
const domain = getConditionDomain(condition);
|
||||
const conditionObjId = getConditionObjectId(condition);
|
||||
label =
|
||||
this.hass.localize(
|
||||
`component.${domain}.conditions.${conditionObjId}.name`
|
||||
) || condition;
|
||||
} else {
|
||||
label =
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${condition}.label` as LocalizeKeys
|
||||
) || condition;
|
||||
}
|
||||
|
||||
return html`<ha-condition-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.condition=${condition}
|
||||
></ha-condition-icon
|
||||
><span slot="headline">${label}</span>`;
|
||||
};
|
||||
|
||||
private _rowRenderer = (item: PickerComboBoxItem) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<ha-condition-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.condition=${item.search_labels!.condition || undefined}
|
||||
></ha-condition-icon>
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
private _processedTypes = memoizeOne(
|
||||
(
|
||||
conditionDescriptions: ConditionDescriptions,
|
||||
localize: LocalizeFunc
|
||||
): [string, string, string][] => {
|
||||
(conditionDescriptions: ConditionDescriptions, localize: LocalizeFunc) => {
|
||||
const legacy = (
|
||||
Object.keys(CONDITION_ICONS) as (keyof typeof CONDITION_ICONS)[]
|
||||
).map(
|
||||
(condition) =>
|
||||
[
|
||||
).map((condition) => {
|
||||
const primary = localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
|
||||
);
|
||||
return {
|
||||
id: condition,
|
||||
primary,
|
||||
sorting_label: primary,
|
||||
search_labels: {
|
||||
condition,
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
|
||||
),
|
||||
condition,
|
||||
] as [string, string, string]
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
const platform = Object.keys(conditionDescriptions).map((condition) => {
|
||||
const domain = getConditionDomain(condition);
|
||||
const conditionObjId = getConditionObjectId(condition);
|
||||
return [
|
||||
`${DYNAMIC_PREFIX}${condition}`,
|
||||
const primary =
|
||||
localize(`component.${domain}.conditions.${conditionObjId}.name`) ||
|
||||
condition;
|
||||
return {
|
||||
id: `${DYNAMIC_PREFIX}${condition}`,
|
||||
primary,
|
||||
secondary: domainToName(this.hass.localize, domain),
|
||||
sorting_label: primary,
|
||||
search_labels: {
|
||||
condition,
|
||||
condition,
|
||||
] as [string, string, string];
|
||||
domain,
|
||||
},
|
||||
};
|
||||
});
|
||||
return [...legacy, ...platform].sort((a, b) =>
|
||||
stringCompare(a[1], b[1], this.hass.locale.language)
|
||||
);
|
||||
return () => [...legacy, ...platform];
|
||||
}
|
||||
);
|
||||
|
||||
@@ -201,7 +232,8 @@ export class HaConditionAction
|
||||
});
|
||||
}
|
||||
|
||||
private _typeChanged(ev: HaSelectSelectEvent) {
|
||||
private _typeChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const type = ev.detail.value;
|
||||
|
||||
if (!type) {
|
||||
@@ -249,7 +281,7 @@ export class HaConditionAction
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-select {
|
||||
ha-generic-picker {
|
||||
margin-bottom: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-toggle-group";
|
||||
import "../../../components/ha-combo-box-item";
|
||||
import { CONDITION_ICONS } from "../../../components/ha-condition-icon";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-header";
|
||||
import "../../../components/ha-domain-icon";
|
||||
import "../../../components/ha-floor-icon";
|
||||
@@ -51,7 +52,6 @@ import "../../../components/ha-section-title";
|
||||
import "../../../components/ha-service-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { TRIGGER_ICONS } from "../../../components/ha-trigger-icon";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/search-input";
|
||||
import {
|
||||
ACTION_BUILDING_BLOCKS_GROUP,
|
||||
@@ -756,19 +756,16 @@ class DialogAddAutomationElement
|
||||
${this._renderDialogSubtitle()}
|
||||
${!this._narrow || (!this._selectedGroup && !this._selectedTarget)
|
||||
? html`
|
||||
<a
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircleOutline}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${this._params!.type}s.learn_more`
|
||||
)}
|
||||
slot="actionItems"
|
||||
href=${docUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircleOutline}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${this._params!.type}s.learn_more`
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
${this._narrow && (this._selectedGroup || this._selectedTarget)
|
||||
@@ -2100,7 +2097,7 @@ class DialogAddAutomationElement
|
||||
--ha-dialog-max-height: var(--ha-dialog-min-height);
|
||||
}
|
||||
|
||||
ha-dialog a[slot="actionItems"] {
|
||||
ha-dialog ha-icon-button[slot="actionItems"] {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-textfield";
|
||||
|
||||
import {
|
||||
@@ -73,19 +73,16 @@ class DialogAutomationMode extends LitElement implements HassDialog {
|
||||
header-title=${title}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<a
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.modes.learn_more"
|
||||
)}
|
||||
.path=${mdiHelpCircleOutline}
|
||||
href=${documentationUrl(this.hass, "/docs/automation/modes/")}
|
||||
slot="headerActionItems"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.modes.learn_more"
|
||||
)}
|
||||
.path=${mdiHelpCircleOutline}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
></ha-icon-button>
|
||||
<ha-md-list
|
||||
role="listbox"
|
||||
tabindex="0"
|
||||
@@ -213,6 +210,9 @@ class DialogAutomationMode extends LitElement implements HassDialog {
|
||||
.options {
|
||||
padding: 0 24px 24px 24px;
|
||||
}
|
||||
ha-wa-dialog ha-icon-button[slot="headerActionItems"] {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -221,12 +221,6 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
)
|
||||
)
|
||||
: nothing}
|
||||
${this._renderOptionalChip(
|
||||
"area",
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.dialog.add_area"
|
||||
)
|
||||
)}
|
||||
${this._renderOptionalChip(
|
||||
"category",
|
||||
this.hass.localize(
|
||||
@@ -239,6 +233,12 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
"ui.panel.config.automation.editor.dialog.add_labels"
|
||||
)
|
||||
)}
|
||||
${this._renderOptionalChip(
|
||||
"area",
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.dialog.add_area"
|
||||
)
|
||||
)}
|
||||
</ha-chip-set>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -345,12 +345,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
`,
|
||||
},
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
title: "",
|
||||
label: this.hass.localize("ui.panel.config.generic.headers.actions"),
|
||||
type: "icon-button",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
template: (automation) => html`
|
||||
<ha-icon-button
|
||||
.automation=${automation}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../../components/ha-textfield";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import type { ConversationTrigger } from "../../../../../data/automation";
|
||||
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
@@ -163,7 +163,7 @@ export class HaConversationTrigger
|
||||
ha-textfield > ha-icon-button {
|
||||
position: relative;
|
||||
right: -8px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
|
||||
@@ -27,7 +27,6 @@ export class HaNumericStateTrigger extends LitElement {
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
entityId: string | string[],
|
||||
inputAboveIsEntity?: boolean,
|
||||
inputBelowIsEntity?: boolean
|
||||
) =>
|
||||
@@ -39,9 +38,9 @@ export class HaNumericStateTrigger extends LitElement {
|
||||
},
|
||||
{
|
||||
name: "attribute",
|
||||
context: { filter_entity: "entity_id" },
|
||||
selector: {
|
||||
attribute: {
|
||||
entity_id: entityId ? entityId[0] : undefined,
|
||||
hide_attributes: [
|
||||
"access_token",
|
||||
"auto_update",
|
||||
@@ -275,7 +274,6 @@ export class HaNumericStateTrigger extends LitElement {
|
||||
public render() {
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
this.trigger.entity_id,
|
||||
this._inputAboveIsEntity,
|
||||
this._inputBelowIsEntity
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { slugify } from "../../../../../common/string/slugify";
|
||||
import { copyToClipboard } from "../../../../../common/util/copy-clipboard";
|
||||
import "../../../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../../../components/ha-dropdown";
|
||||
import "../../../../../components/ha-dropdown-item";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-textfield";
|
||||
@@ -19,7 +20,6 @@ import type {
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { showToast } from "../../../../../util/toast";
|
||||
import { handleChangeEvent } from "../ha-automation-trigger-row";
|
||||
import type { HaDropdownSelectEvent } from "../../../../../components/ha-dropdown";
|
||||
|
||||
const SUPPORTED_METHODS = ["GET", "HEAD", "POST", "PUT"];
|
||||
const DEFAULT_METHODS = ["POST", "PUT"];
|
||||
@@ -234,7 +234,7 @@ export class HaWebhookTrigger extends LitElement {
|
||||
}
|
||||
|
||||
ha-textfield > ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--ha-icon-button-size: 24px;
|
||||
--mdc-icon-size: 18px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
@@ -213,7 +213,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._stepTitle}
|
||||
width="medium"
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
|
||||
@@ -140,7 +140,6 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${dialogTitle}
|
||||
width="medium"
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div class="content">
|
||||
|
||||
@@ -90,7 +90,6 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${dialogTitle}
|
||||
width="medium"
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user