Compare commits

...
Author SHA1 Message Date
Wendelin 0d0cdfbde1 Remove unused stuff 2025-03-06 12:24:41 +01:00
Wendelin 97f8aac984 Change to empty card 2025-03-06 12:19:24 +01:00
Wendelin 6d9c7eb52a Fix prettier 2025-02-26 16:45:08 +01:00
Wendelin 311143d6a1 Only allow spacing card in sections 2025-02-26 16:40:00 +01:00
Wendelin b9efdfa10e Add edit styling 2025-02-26 16:39:02 +01:00
Wendelin b64042186f Add spacing card 2025-02-26 14:35:53 +01:00
Bram KragtenandGitHub cf288f7cd1 Don't allow history to be picked for backup when db is not in default … (#24402) 2025-02-26 14:09:46 +01:00
Bram KragtenandGitHub 1c8284609f Update translation keys for sub entry flows (#24400) 2025-02-26 13:38:21 +01:00
Petar PetrovandGitHub 1fdadbf1b8 Add drag to zoom & double click in echarts (#24401) 2025-02-26 12:33:39 +01:00
karwostsandGitHub 3b272ae411 Allow entity-filter-card to filter on other entity (#24396) 2025-02-25 22:37:31 +01:00
a048c36861 ha-tabs: adapt _affectScroll() to RTL (#24018)
* adapt _affectScroll for RTL

* eslint

* import "property"

* prettier

* Update ha-tabs.ts

This makes this work...

* Lint

---------

Co-authored-by: Yosi Levy <[email protected]>
2025-02-25 22:20:57 +01:00
Paul BotteinandGitHub 2a6c1773f3 Update responsive layout svg (#24391) 2025-02-25 18:58:46 +01:00
Paul BotteinandGitHub bf17012753 Fix select box svg icon in dark mode (#24389) 2025-02-25 16:52:02 +01:00
WendelinandGitHub bbaf23e049 Add lovelace bg opacity preview (#24387)
* Add lovelace bg opacity preview

* Add yaml preview

* Simplify logic with css var

* Clean up

* Remove important

* Add opacity default to 100
2025-02-25 16:21:32 +01:00
34b7929165 Improve labels when there no mqtt entities or triggers debug info yet (#24383)
* Improve labels when there no mqtt entities or triggers debug info yet

* Update src/panels/config/devices/device-detail/integration-elements/mqtt/dialog-mqtt-device-debug-info.ts

---------

Co-authored-by: Bram Kragten <[email protected]>
2025-02-25 15:11:19 +01:00
Paul BotteinandGitHub 8f06e70a11 Add view top margin option for section view (#24386) 2025-02-25 15:06:13 +01:00
Jan-Philipp BeneckeandGitHub 50ac60b35e Improve area registry dialog user experience (#24086)
* Improve area registry dialog user experience

* Simplify expanded

* Improve heading and description

* Fix padding and remove secondary

* Process code review

* Remove more unused imports

* Re-add alias description
2025-02-25 15:04:30 +01:00
Petar PetrovandGitHub 4caca19e32 Migrate sankey chart to echarts (#24185)
* Migrate sankey chart to echarts

* don't memoize options

* Dynamically load echart sankey component

* load Sankey component with the card

* load the sankey component in ha-chart-base
2025-02-25 15:02:41 +01:00
a7b1c45c00 Replace simple-tooltip with ha-tooltip (#24384)
* Start with simple-tooltip migration

* Remove simple-tooltip

* Fix tooltip in hassio-repositories

* Remove space

* Update hassio/src/dialogs/repositories/dialog-hassio-repositories.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update src/components/ha-icon-overflow-menu.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update src/components/ha-target-picker.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update src/components/media-player/ha-media-player-browse.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update src/components/ha-target-picker.ts

Co-authored-by: Bram Kragten <[email protected]>

* Fix content props

* Use ha-tooltip in data-table-icon

* Update src/panels/config/areas/ha-config-area-page.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update src/panels/config/devices/ha-config-device-page.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update src/panels/config/integrations/ha-integration-card.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update src/panels/config/integrations/ha-integration-card.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update src/panels/config/integrations/ha-integration-list-item.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update src/panels/config/integrations/ha-integration-list-item.ts

Co-authored-by: Bram Kragten <[email protected]>

---------

Co-authored-by: Bram Kragten <[email protected]>
2025-02-25 14:58:38 +01:00
Paul BotteinandGitHub 10c3e4c6f8 Rename appearance section to content in card/badge editors (#24385) 2025-02-25 14:56:54 +01:00
Petar PetrovandGitHub 1f50c359dc Fix history Y axis with tiny values (#24342)
* Fix history Y axis with tiny values

* set min fractions to 2

* handle negative values
2025-02-25 12:00:33 +01:00
a3e24a3dc0 Add view header (#24237)
* Create basic section heading

* Update badge position

* Don't allow to move card

* Remove view badges in section

* Improve heading section UX

* Add basic editor for heading section

* Add badges position option

* Add layout and badge position

* Improve button

* Use layout

* Simplify edit mode

* Fix CSS

* Fix add heading button

* Improve badges

* Rename to extra space

* Fix add top badge position

* Add migration

* Fix delete section confirmation

* Add translations

* Update comment

* Add header config to view

* Add edit card overlay

* Remove badge support for sections

* Remove section badges

* Clean section

* Fix header visibility

* Update translations

* Add view header editor

* Use new markdown card option

* Revert useless changes

* Add options translations

* Use edit dialog

* Unify font

* Update default text

* Share default between editor and view

* Update src/panels/lovelace/editor/view-header/hui-dialog-edit-view-header.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update src/panels/lovelace/editor/view-header/hui-dialog-edit-view-header.ts

Co-authored-by: Bram Kragten <[email protected]>

* Fix comment

* Use select box for view header editor

* Remove extra space option

* Update select box

---------

Co-authored-by: Bram Kragten <[email protected]>
2025-02-25 11:43:54 +01:00
Bram KragtenandGitHub a906285a03 Use automatic_backups_configured flag to signal if backup onboardin… (#24367)
* Use `automatic_backups_configured` flag to signal if backup onboarding was done

* fix

* dont show a newly created password without showing it
2025-02-25 11:42:46 +01:00
5c933a43b2 Custom chart legend (#24227)
* Custom chart legend

* limit legend label length

* fade long legends

* tweak margin

* new design

* fix margins

* lighter background

* fix variable height charts

* tweak legend button

* lint

* switch to secondary-text-color

* Card option to expand legend

* Update src/components/chart/ha-chart-base.ts

Co-authored-by: Wendelin <[email protected]>

* pr comments

* use ha-assist-chip

* pr comments

* Apply suggestions from code review

Co-authored-by: Bram Kragten <[email protected]>

---------

Co-authored-by: Wendelin <[email protected]>
Co-authored-by: Bram Kragten <[email protected]>
2025-02-25 12:00:50 +02:00
Petar PetrovandGitHub 151a7fbc40 Z-WaveJS: Delay fetching new config values after addition (#24317) 2025-02-25 10:44:45 +01:00
Petar PetrovandGitHub 4fa915c869 Z-WaveJS: stop inclusion subscription while dialog is open (#24294)
* Z-WaveJS: stop inclusion subscription while dialog is open and handling it

* Don't pass dsk to core

* PR comments
2025-02-25 10:43:56 +01:00
Paul BotteinandGitHub d47e5c847b Add RTL and dark mode support for select box image (#24374)
Add rtl and dark for select box
2025-02-25 10:38:02 +01:00
Flo EdelmannandGitHub db5036aed3 Support limits in sensor card editor (#24358)
* Support `limits` in sensor card editor

* Add new translation keys for limit min/max
2025-02-25 09:19:13 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
bb672d0272 Update dependency prettier to v3.5.2 (#24382)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 08:36:20 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
e26d3d39f0 Update dependency eslint to v9.21.0 (#24381)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 01:36:23 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
e54c3a69af Update dependency @lokalise/node-api to v13.2.1 (#24377)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 19:59:53 +01:00
karwostsandGitHub cc04457d72 Add 'last seen' to BT advertisement montior (#24361)
* Add 'last seen' to BT advertisement montior

* use ha-relative-time
2025-02-24 17:29:34 +02:00
WendelinandGitHub 9e1d64e728 shoelace tooltip (#24337)
* Add shoelace based ha-tooltip

* Use shoelace component

* Improve styles

* Add docs

* Fix tooltip docs

* Revert new global styles
2025-02-24 15:37:59 +01:00
Paul BotteinandGitHub 0cfe7f8d12 Tile card editor improvements (#24373)
* Add selector support

* Feedbacks

* Use select box fields in tile card editor
2025-02-24 14:26:55 +00:00
karwostsandGitHub 2b1f301db6 Push map strategy logic down into map card (#24303) 2025-02-24 15:13:52 +01:00
Paul BotteinandGitHub fc4996412e Add select box component (#24370)
* Add select box component

* Add selector support

* Use it in markdown card

* Add select box to gallery

* Feedbacks
2025-02-24 15:07:51 +01:00
Jan-Philipp BeneckeandGitHub ece4a6345f Use custom styling for cluster marker (#24371)
* Use custom styling for cluster marker

* Process code review
2025-02-24 13:11:06 +00:00
Bram KragtenandGitHub a4c08a78b9 Check for updated frontend on connect too (#24368) 2025-02-24 13:16:54 +02:00
Joakim SørensenandGitHub a438fc5e41 Add connection check and dialog with results for cloud login (#24301) 2025-02-24 09:37:17 +01:00
karwostsandGitHub 783132ae46 Fix solar order in compare stack for usage graph (#24360)
* Fix solar order in compare stack for usage graph

* remove accidental commit
2025-02-24 09:08:55 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
680d81001c Update rspack monorepo to v1.2.5 (#24353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 09:00:19 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a917383d7a Bump actions/cache from 4.2.0 to 4.2.1 (#24366)
Bumps [actions/cache](https://github.com/actions/cache) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4.2.0...v4.2.1)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 08:59:27 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
455a6761cd Bump actions/upload-artifact from 4.6.0 to 4.6.1 (#24365)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.6.0...v4.6.1)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 08:58:42 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
acf42d7637 Update dependency globals to v16 (#24359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 08:56:48 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3857c7321a Update dependency eslint-plugin-wc to v2.2.1 (#24362)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 07:08:27 +01:00
puddlyandGitHub 5eec814988 Hide hardware integrations from the "add integration" dialog (#24345) 2025-02-22 08:43:18 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
edd37565a6 Update vitest monorepo to v3.0.6 (#24344)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 19:03:37 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
fb3f779121 Update rspack monorepo to v1.2.4 (#24343)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 19:03:00 +01:00
WendelinandGitHub 4d7634ac67 Landing-page: ping supervisor before get network infos (#24330)
* Ping supervisor before get network infos

* Rename supervisor proxy prefix
2025-02-21 08:14:10 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Simon Lamon
ba5c1133c6 Lock file maintenance (#24306)
* Lock file maintenance

* Bump codemirror view to 6.36.3

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Simon Lamon <[email protected]>
2025-02-20 19:36:32 +00:00
WendelinandGitHub 0a05dd8f71 Add more tests for common/entity (#24336)
* Use substring instead of deprecated substr

* Add more common entity tests
2025-02-20 20:11:53 +01:00
400106ec09 Adjust WebSocket ping timeout to 15 seconds (#24339)
* Adjust WebSocket ping timeout to 15 seconds

5 seconds was too low to prevent the UI from reloading
when connecting the WebSocket during startup or on
a high latancy connection

This problem presented as the UI reloading over
and over again because it could never respond
to the ping in time on high latancy connections.

At startup it usually only did this once so it
went unnoticed in most cases.

This ping was added in #18934

* Update connection-mixin.ts

Co-authored-by: Jan-Philipp Benecke <[email protected]>

---------

Co-authored-by: Jan-Philipp Benecke <[email protected]>
2025-02-20 20:09:51 +01:00
Jan-Philipp BeneckeandGitHub a7a4194e09 Add tile card feature for counter actions (#24340)
* Add tile card feature for counter actions

* Format

* Change icon

* Disable buttons when hit limit

* Change increment/decrement icons
2025-02-20 19:09:44 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
0bd7d27c57 Update dependency @lokalise/node-api to v13.2.0 (#24335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 16:14:54 +01:00
Jan-Philipp BeneckeandGitHub 8175e45921 Rename switch-toggle feature to toggle and improve (#24333)
* Rename `switch-toggle` feature to `toggle` and improve

* Format
2025-02-20 14:51:49 +01:00
Jan-Philipp BeneckeandGitHub cae36b393b Focus alarm control panel PIN input on wider screens (#24324)
* Focus alarm control panel PIN input on wider screens

* Also apply on textfield
2025-02-20 15:20:28 +02:00
Paul BotteinandGitHub f84ad92356 Extract saving card config from card editor (#24319)
* Extract saving card config from card editor

* Await

* Add try/catch

* Remove unused translations

* Remove duration
2025-02-20 12:27:39 +01:00
WendelinandGitHub fb1ee2ed1d Remove toggles from ha-icon-button (#24331) 2025-02-20 12:14:40 +01:00
Paul BotteinandGitHub 9073282174 Add text only style to markdown card (#24329) 2025-02-20 11:40:39 +01:00
Jan-Philipp BeneckeandGitHub 91bd5cba08 Add switch toggle feature to tile card (#24325)
* Add tile switch toggle feature

* Remove _currentState
2025-02-20 10:16:14 +02:00
karwostsandGitHub a68bdbfe08 Fix siren advanced controls (#24318) 2025-02-20 08:50:00 +01:00
Jan-Philipp BeneckeandGitHub f3d614b0d3 Make quick bar more keyboard accessible (#24321) 2025-02-20 08:44:49 +01:00
karwostsandGitHub f3c9e4a4a0 Fix catching errors in alarm-control-panel more-info (#24328) 2025-02-20 08:42:17 +01:00
karwostsandGitHub d22a82c4a6 Teardown and rebuild element editor when switching stack cards (#24065) 2025-02-20 07:57:34 +01:00
Jan-Philipp BeneckeandGitHub 5cddc6e5c6 Decrease max cluster radius (#24322) 2025-02-19 21:34:49 +02:00
Jan-Philipp BeneckeandGitHub c5c067ef19 Create copyable textfield component (#24247) 2025-02-19 15:56:29 +01:00
Paul BotteinandGitHub 694bb3088c Improve margin for inline tile card feature (#24316) 2025-02-19 16:07:27 +02:00
Petar PetrovandGitHub ad487470fd Enable downsampling in echarts (#24311)
* Enable downsampling in echarts

* remove unneeded symbol set
2025-02-19 16:05:32 +02:00
176 changed files with 5614 additions and 2191 deletions
+3 -3
View File
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/[email protected].0
uses: actions/[email protected].1
with:
path: |
node_modules/.cache/prettier
@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/[email protected].0
uses: actions/[email protected].1
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/[email protected].0
uses: actions/[email protected].1
with:
name: supervisor-bundle-stats
path: build/stats/*.json
+2 -2
View File
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/[email protected].0
uses: actions/[email protected].1
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/[email protected].0
uses: actions/[email protected].1
with:
name: translations
path: translations.tar.gz
-4
View File
@@ -94,10 +94,6 @@ function copyMapPanel(staticDir) {
npmPath("leaflet.markercluster/dist/MarkerCluster.css"),
staticPath("images/leaflet/")
);
copyFileDir(
npmPath("leaflet.markercluster/dist/MarkerCluster.Default.css"),
staticPath("images/leaflet/")
);
fs.copySync(
npmPath("leaflet/dist/images"),
staticPath("images/leaflet/images/")
+10
View File
@@ -0,0 +1,10 @@
<svg width="94" height="64" viewBox="0 0 94 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="64" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="63" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H31C32.6569 24 34 25.3431 34 27V29C34 30.6569 32.6569 32 31 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M38 27C38 25.3431 39.3431 24 41 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H41C39.3431 32 38 30.6569 38 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M8 39C8 37.3431 9.34315 36 11 36H53C54.6569 36 56 37.3431 56 39V41C56 42.6569 54.6569 44 53 44H11C9.34315 44 8 42.6569 8 41V39Z" fill="black" fill-opacity="0.12"/>
<path d="M60 39C60 37.3431 61.3431 36 63 36H83C84.6569 36 86 37.3431 86 39V41C86 42.6569 84.6569 44 83 44H63C61.3431 44 60 42.6569 60 41V39Z" fill="black" fill-opacity="0.12"/>
<path d="M8 51C8 49.3431 9.34315 48 11 48H31C32.6569 48 34 49.3431 34 51V53C34 54.6569 32.6569 56 31 56H11C9.34315 56 8 54.6569 8 53V51Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,7 @@
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 11C0 9.34315 1.34315 8 3 8H23C24.6569 8 26 9.34315 26 11V13C26 14.6569 24.6569 16 23 16H3C1.34315 16 0 14.6569 0 13V11Z" fill="black" fill-opacity="0.12"/>
<path d="M30 11C30 9.34315 31.3431 8 33 8H91C92.6569 8 94 9.34315 94 11V13C94 14.6569 92.6569 16 91 16H33C31.3431 16 30 14.6569 30 13V11Z" fill="black" fill-opacity="0.12"/>
<path d="M0 23C0 21.3431 1.34315 20 3 20H61C62.6569 20 64 21.3431 64 23V25C64 26.6569 62.6569 28 61 28H3C1.34315 28 0 26.6569 0 25V23Z" fill="black" fill-opacity="0.12"/>
<path d="M68 23C68 21.3431 69.3431 20 71 20H91C92.6569 20 94 21.3431 94 23V25C94 26.6569 92.6569 28 91 28H71C69.3431 28 68 26.6569 68 25V23Z" fill="black" fill-opacity="0.12"/>
<path d="M0 35C0 33.3431 1.34315 32 3 32H23C24.6569 32 26 33.3431 26 35V37C26 38.6569 24.6569 40 23 40H3C1.34315 40 0 38.6569 0 37V35Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 964 B

@@ -0,0 +1,3 @@
---
title: Select box
---
@@ -0,0 +1,152 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-select-box";
import type { SelectBoxOption } from "../../../../src/components/ha-select-box";
const basicOptions: SelectBoxOption[] = [
{
value: "text-only",
label: "Text only",
},
{
value: "card",
label: "Card",
},
{
value: "disabled",
label: "Disabled option",
disabled: true,
},
];
const fullOptions: SelectBoxOption[] = [
{
value: "text-only",
label: "Text only",
description: "Only text, no border and background",
image: "/images/select_box/text_only.svg",
},
{
value: "card",
label: "Card",
description: "With border and background",
image: "/images/select_box/card.svg",
},
{
value: "disabled",
label: "Disabled",
description: "Option that can not be selected",
disabled: true,
},
];
const selects: {
id: string;
label: string;
class?: string;
options: SelectBoxOption[];
disabled?: boolean;
}[] = [
{
id: "basic",
label: "Basic",
options: basicOptions,
},
{
id: "full",
label: "With description and image",
options: fullOptions,
},
];
@customElement("demo-components-ha-select-box")
export class DemoHaSelectBox extends LitElement {
@state() private value?: string = "off";
handleValueChanged(e: CustomEvent) {
this.value = e.detail.value as string;
}
protected render(): TemplateResult {
return html`
${repeat(selects, (select) => {
const { id, label, options } = select;
return html`
<ha-card>
<div class="card-content">
<label id=${id}>${label}</label>
<ha-select-box
.value=${this.value}
.options=${options}
@value-changed=${this.handleValueChanged}
>
</ha-select-box>
</div>
</ha-card>
`;
})}
<ha-card>
<div class="card-content">
<p class="title"><b>Column layout</b></p>
<div class="vertical-selects">
${repeat(selects, (select) => {
const { options } = select;
return html`
<ha-select-box
.value=${this.value}
.options=${options}
.maxColumns=${1}
@value-changed=${this.handleValueChanged}
>
</ha-select-box>
`;
})}
</div>
</div>
</ha-card>
`;
}
static styles = css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
pre {
margin-top: 0;
margin-bottom: 8px;
}
p {
margin: 0;
}
label {
font-weight: 600;
margin-bottom: 8px;
display: block;
}
.custom {
--mdc-icon-size: 24px;
--control-select-color: var(--state-fan-active-color);
--control-select-thickness: 130px;
--control-select-border-radius: 36px;
}
p.title {
margin-bottom: 12px;
}
.vertical-selects ha-select-box {
display: block;
margin-bottom: 24px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-select-box": DemoHaSelectBox;
}
}
@@ -0,0 +1,30 @@
---
title: Tooltip
---
A tooltip's target is its _first child element_, so you should only wrap one element inside of the tooltip. If you need the tooltip to show up for multiple elements, nest them inside a container first.
Tooltips use `display: contents` so they won't interfere with how elements are positioned in a flex or grid layout.
<ha-tooltip content="This is a tooltip">
<ha-button>Hover Me</ha-button>
</ha-tooltip>
```
<ha-tooltip content="This is a tooltip">
<ha-button>Hover Me</ha-button>
</ha-tooltip>
```
## Documentation
This element is based on sholace `sl-tooltip` it only sets some css tokens and has a custom show/hide animation.
<a href="https://shoelace.style/components/tooltip" target="_blank" rel="noopener noreferrer">Shoelace documentation</a>
### HA style tokens
In your theme settings use this without the prefixed `--`.
- `--ha-tooltip-border-radius` (Default: 4px)
- `--ha-tooltip-arrow-size` (Default: 8px)
@@ -0,0 +1,2 @@
import "../../../../src/components/ha-tooltip";
import "../../../../src/components/ha-button";
-4
View File
@@ -253,13 +253,9 @@ export class HassioBackups extends LitElement {
"backup.delete_selected"
)}
.path=${mdiDelete}
id="delete-btn"
class="warning"
@click=${this._deleteSelected}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="delete-btn">
${this.supervisor.localize("backup.delete_selected")}
</simple-tooltip>
`}
</div>
</div> `
@@ -1,6 +1,5 @@
import "@material/mwc-button/mwc-button";
import { mdiDelete, mdiDeleteOff } from "@mdi/js";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -8,6 +7,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-tooltip";
import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-icon-button";
@@ -118,28 +118,27 @@ class HassioRepositoriesDialog extends LitElement {
<div>${repo.maintainer}</div>
<div>${repo.url}</div>
</div>
<div class="delete" slot="end">
<ha-icon-button
.disabled=${usedRepositories.includes(repo.slug)}
.slug=${repo.slug}
.path=${usedRepositories.includes(repo.slug)
? mdiDeleteOff
: mdiDelete}
@click=${this._removeRepository}
>
</ha-icon-button>
<simple-tooltip
animation-delay="0"
position="bottom"
offset="1"
>
${this._dialogParams!.supervisor.localize(
usedRepositories.includes(repo.slug)
? "dialog.repositories.used"
: "dialog.repositories.remove"
)}
</simple-tooltip>
</div>
<ha-tooltip
class="delete"
slot="end"
.content=${this._dialogParams!.supervisor.localize(
usedRepositories.includes(repo.slug)
? "dialog.repositories.used"
: "dialog.repositories.remove"
)}
>
<div>
<ha-icon-button
.disabled=${usedRepositories.includes(repo.slug)}
.slug=${repo.slug}
.path=${usedRepositories.includes(repo.slug)
? mdiDeleteOff
: mdiDelete}
@click=${this._removeRepository}
>
</ha-icon-button>
</div>
</ha-tooltip>
</ha-md-list-item>
`
)
@@ -17,6 +17,7 @@ import "../../../src/components/ha-alert";
import {
ALTERNATIVE_DNS_SERVERS,
getSupervisorNetworkInfo,
pingSupervisor,
setSupervisorNetworkDns,
} from "../data/supervisor";
import { fireEvent } from "../../../src/common/dom/fire_event";
@@ -85,7 +86,28 @@ class LandingPageNetwork extends LitElement {
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
this._fetchSupervisorInfo();
this._pingSupervisor();
}
private _schedulePingSupervisor() {
setTimeout(
() => this._pingSupervisor(),
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
);
}
private async _pingSupervisor() {
try {
const response = await pingSupervisor();
if (!response.ok) {
throw new Error("Failed to ping supervisor, assume update in progress");
}
this._fetchSupervisorInfo();
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
this._schedulePingSupervisor();
}
}
private _scheduleFetchSupervisorInfo() {
+8 -4
View File
@@ -18,7 +18,7 @@ export const ALTERNATIVE_DNS_SERVERS: {
];
export async function getSupervisorLogs(lines = 100) {
return fetch(`/supervisor/supervisor/logs?lines=${lines}`, {
return fetch(`/supervisor-api/supervisor/logs?lines=${lines}`, {
headers: {
Accept: "text/plain",
},
@@ -26,22 +26,26 @@ export async function getSupervisorLogs(lines = 100) {
}
export async function getSupervisorLogsFollow(lines = 500) {
return fetch(`/supervisor/supervisor/logs/follow?lines=${lines}`, {
return fetch(`/supervisor-api/supervisor/logs/follow?lines=${lines}`, {
headers: {
Accept: "text/plain",
},
});
}
export async function pingSupervisor() {
return fetch("/supervisor-api/supervisor/ping");
}
export async function getSupervisorNetworkInfo() {
return fetch("/supervisor/network/info");
return fetch("/supervisor-api/network/info");
}
export const setSupervisorNetworkDns = async (
dnsServerIndex: number,
primaryInterface: string
) =>
fetch(`/supervisor/network/interface/${primaryInterface}/update`, {
fetch(`/supervisor-api/network/interface/${primaryInterface}/update`, {
method: "POST",
body: JSON.stringify({
ipv4: {
+12 -11
View File
@@ -34,7 +34,7 @@
"@codemirror/legacy-modes": "6.4.3",
"@codemirror/search": "6.5.9",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.36.2",
"@codemirror/view": "6.36.3",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.17.3",
"@formatjs/intl-displaynames": "6.8.10",
@@ -56,7 +56,6 @@
"@lit-labs/motion": "1.0.8",
"@lit-labs/observers": "2.0.5",
"@lit-labs/virtualizer": "2.1.0",
"@lrnwebcomponents/simple-tooltip": "8.0.2",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-base": "0.27.0",
@@ -90,6 +89,7 @@
"@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.2",
"@replit/codemirror-indentation-markers": "6.5.3",
"@shoelace-style/shoelace": "2.20.0",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.6.5",
"@vaadin/vaadin-themable-mixin": "24.6.5",
@@ -161,13 +161,13 @@
"@babel/preset-env": "7.26.9",
"@babel/preset-typescript": "7.26.0",
"@bundle-stats/plugin-webpack-filter": "4.18.2",
"@lokalise/node-api": "13.1.0",
"@lokalise/node-api": "13.2.1",
"@octokit/auth-oauth-device": "7.1.3",
"@octokit/plugin-retry": "7.1.4",
"@octokit/rest": "21.1.1",
"@rsdoctor/rspack-plugin": "0.4.13",
"@rspack/cli": "1.2.3",
"@rspack/core": "1.2.3",
"@rspack/cli": "1.2.5",
"@rspack/core": "1.2.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.21",
"@types/chromecast-caf-sender": "1.0.11",
@@ -186,12 +186,12 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "3.0.5",
"@vitest/coverage-v8": "3.0.6",
"babel-loader": "9.2.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.20.1",
"eslint": "9.21.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.0.1",
"eslint-import-resolver-webpack": "0.13.10",
@@ -199,7 +199,7 @@
"eslint-plugin-lit": "1.15.0",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "2.2.0",
"eslint-plugin-wc": "2.2.1",
"fancy-log": "2.0.0",
"fs-extra": "11.3.0",
"glob": "11.0.1",
@@ -217,7 +217,7 @@
"lodash.template": "4.5.0",
"map-stream": "0.0.7",
"pinst": "3.0.0",
"prettier": "3.5.1",
"prettier": "3.5.2",
"rspack-manifest-plugin": "5.0.3",
"serve": "14.2.4",
"sinon": "19.0.2",
@@ -226,7 +226,8 @@
"ts-lit-plugin": "2.0.2",
"typescript": "5.7.3",
"typescript-eslint": "8.24.1",
"vitest": "3.0.5",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.0.6",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
@@ -240,7 +241,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15",
"globals": "15.15.0",
"globals": "16.0.0",
"tslib": "2.8.1"
},
"packageManager": "[email protected]"
@@ -0,0 +1,10 @@
<svg width="94" height="64" viewBox="0 0 94 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="64" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="63" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H31C32.6569 24 34 25.3431 34 27V29C34 30.6569 32.6569 32 31 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M38 27C38 25.3431 39.3431 24 41 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H41C39.3431 32 38 30.6569 38 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M8 39C8 37.3431 9.34315 36 11 36H53C54.6569 36 56 37.3431 56 39V41C56 42.6569 54.6569 44 53 44H11C9.34315 44 8 42.6569 8 41V39Z" fill="black" fill-opacity="0.12"/>
<path d="M60 39C60 37.3431 61.3431 36 63 36H83C84.6569 36 86 37.3431 86 39V41C86 42.6569 84.6569 44 83 44H63C61.3431 44 60 42.6569 60 41V39Z" fill="black" fill-opacity="0.12"/>
<path d="M8 51C8 49.3431 9.34315 48 11 48H31C32.6569 48 34 49.3431 34 51V53C34 54.6569 32.6569 56 31 56H11C9.34315 56 8 54.6569 8 53V51Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,10 @@
<svg width="94" height="64" viewBox="0 0 94 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V56C94 60.4183 90.4183 64 86 64H8C3.58172 64 0 60.4183 0 56V8Z" fill="#1C1C1C"/>
<path d="M0.5 8C0.5 3.85786 3.85786 0.5 8 0.5H86C90.1421 0.5 93.5 3.85786 93.5 8V56C93.5 60.1421 90.1421 63.5 86 63.5H8C3.85786 63.5 0.5 60.1421 0.5 56V8Z" stroke="white" stroke-opacity="0.24"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="white" fill-opacity="0.48"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H31C32.6569 24 34 25.3431 34 27V29C34 30.6569 32.6569 32 31 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M38 27C38 25.3431 39.3431 24 41 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H41C39.3431 32 38 30.6569 38 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M8 39C8 37.3431 9.34315 36 11 36H53C54.6569 36 56 37.3431 56 39V41C56 42.6569 54.6569 44 53 44H11C9.34315 44 8 42.6569 8 41V39Z" fill="white" fill-opacity="0.24"/>
<path d="M60 39C60 37.3431 61.3431 36 63 36H83C84.6569 36 86 37.3431 86 39V41C86 42.6569 84.6569 44 83 44H63C61.3431 44 60 42.6569 60 41V39Z" fill="white" fill-opacity="0.24"/>
<path d="M8 51C8 49.3431 9.34315 48 11 48H31C32.6569 48 34 49.3431 34 51V53C34 54.6569 32.6569 56 31 56H11C9.34315 56 8 54.6569 8 53V51Z" fill="white" fill-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,7 @@
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 11C0 9.34315 1.34315 8 3 8H23C24.6569 8 26 9.34315 26 11V13C26 14.6569 24.6569 16 23 16H3C1.34315 16 0 14.6569 0 13V11Z" fill="black" fill-opacity="0.12"/>
<path d="M30 11C30 9.34315 31.3431 8 33 8H91C92.6569 8 94 9.34315 94 11V13C94 14.6569 92.6569 16 91 16H33C31.3431 16 30 14.6569 30 13V11Z" fill="black" fill-opacity="0.12"/>
<path d="M0 23C0 21.3431 1.34315 20 3 20H61C62.6569 20 64 21.3431 64 23V25C64 26.6569 62.6569 28 61 28H3C1.34315 28 0 26.6569 0 25V23Z" fill="black" fill-opacity="0.12"/>
<path d="M68 23C68 21.3431 69.3431 20 71 20H91C92.6569 20 94 21.3431 94 23V25C94 26.6569 92.6569 28 91 28H71C69.3431 28 68 26.6569 68 25V23Z" fill="black" fill-opacity="0.12"/>
<path d="M0 35C0 33.3431 1.34315 32 3 32H23C24.6569 32 26 33.3431 26 35V37C26 38.6569 24.6569 40 23 40H3C1.34315 40 0 38.6569 0 37V35Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 964 B

@@ -0,0 +1,7 @@
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 11C0 9.34315 1.34315 8 3 8H23C24.6569 8 26 9.34315 26 11V13C26 14.6569 24.6569 16 23 16H3C1.34315 16 0 14.6569 0 13V11Z" fill="white" fill-opacity="0.24"/>
<path d="M30 11C30 9.34315 31.3431 8 33 8H91C92.6569 8 94 9.34315 94 11V13C94 14.6569 92.6569 16 91 16H33C31.3431 16 30 14.6569 30 13V11Z" fill="white" fill-opacity="0.24"/>
<path d="M0 23C0 21.3431 1.34315 20 3 20H61C62.6569 20 64 21.3431 64 23V25C64 26.6569 62.6569 28 61 28H3C1.34315 28 0 26.6569 0 25V23Z" fill="white" fill-opacity="0.24"/>
<path d="M68 23C68 21.3431 69.3431 20 71 20H91C92.6569 20 94 21.3431 94 23V25C94 26.6569 92.6569 28 91 28H71C69.3431 28 68 26.6569 68 25V23Z" fill="white" fill-opacity="0.24"/>
<path d="M0 35C0 33.3431 1.34315 32 3 32H23C24.6569 32 26 33.3431 26 35V37C26 38.6569 24.6569 40 23 40H3C1.34315 40 0 38.6569 0 37V35Z" fill="white" fill-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 964 B

@@ -0,0 +1,7 @@
<svg width="94" height="40" viewBox="0 0 94 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="40" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="39" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<circle cx="20" cy="20" r="12" fill="black" fill-opacity="0.12"/>
<path d="M40 14C40 10.6863 42.6863 8 46 8H65C68.3137 8 71 10.6863 71 14C71 17.3137 68.3137 20 65 20H46C42.6863 20 40 17.3137 40 14Z" fill="black" fill-opacity="0.32"/>
<path d="M40 28C40 25.7909 41.7909 24 44 24H77C79.2091 24 81 25.7909 81 28C81 30.2091 79.2091 32 77 32H44C41.7909 32 40 30.2091 40 28Z" fill="black" fill-opacity="0.32"/>
</svg>

After

Width:  |  Height:  |  Size: 652 B

@@ -0,0 +1,7 @@
<svg width="94" height="40" viewBox="0 0 94 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="40" rx="8" fill="#1C1C1C"/>
<rect x="0.5" y="0.5" width="93" height="39" rx="7.5" stroke="white" stroke-opacity="0.24"/>
<circle cx="20" cy="20" r="12" fill="white" fill-opacity="0.24"/>
<path d="M40 14C40 10.6863 42.6863 8 46 8H65C68.3137 8 71 10.6863 71 14C71 17.3137 68.3137 20 65 20H46C42.6863 20 40 17.3137 40 14Z" fill="white" fill-opacity="0.48"/>
<path d="M40 28C40 25.7909 41.7909 24 44 24H77C79.2091 24 81 25.7909 81 28C81 30.2091 79.2091 32 77 32H44C41.7909 32 40 30.2091 40 28Z" fill="white" fill-opacity="0.48"/>
</svg>

After

Width:  |  Height:  |  Size: 654 B

@@ -0,0 +1,7 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="72" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="71" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<circle cx="47" cy="20" r="12" fill="black" fill-opacity="0.12"/>
<path d="M31.5 46C31.5 42.6863 34.1863 40 37.5 40H56.5C59.8137 40 62.5 42.6863 62.5 46C62.5 49.3137 59.8137 52 56.5 52H37.5C34.1863 52 31.5 49.3137 31.5 46Z" fill="black" fill-opacity="0.32"/>
<path d="M26.5 60C26.5 57.7909 28.2909 56 30.5 56H63.5C65.7091 56 67.5 57.7909 67.5 60C67.5 62.2091 65.7091 64 63.5 64H30.5C28.2909 64 26.5 62.2091 26.5 60Z" fill="black" fill-opacity="0.32"/>
</svg>

After

Width:  |  Height:  |  Size: 699 B

@@ -0,0 +1,7 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="72" rx="8" fill="#1C1C1C"/>
<rect x="0.5" y="0.5" width="93" height="71" rx="7.5" stroke="white" stroke-opacity="0.24"/>
<circle cx="47" cy="20" r="12" fill="white" fill-opacity="0.24"/>
<path d="M31.5 46C31.5 42.6863 34.1863 40 37.5 40H56.5C59.8137 40 62.5 42.6863 62.5 46C62.5 49.3137 59.8137 52 56.5 52H37.5C34.1863 52 31.5 49.3137 31.5 46Z" fill="white" fill-opacity="0.48"/>
<path d="M26.5 60C26.5 57.7909 28.2909 56 30.5 56H63.5C65.7091 56 67.5 57.7909 67.5 60C67.5 62.2091 65.7091 64 63.5 64H30.5C28.2909 64 26.5 62.2091 26.5 60Z" fill="white" fill-opacity="0.48"/>
</svg>

After

Width:  |  Height:  |  Size: 701 B

@@ -0,0 +1,6 @@
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="48" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="47" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<rect x="8" y="8" width="78" height="12" rx="3" fill="black" fill-opacity="0.12"/>
<rect x="8" y="28" width="78" height="12" rx="3" fill="black" fill-opacity="0.32"/>
</svg>

After

Width:  |  Height:  |  Size: 414 B

@@ -0,0 +1,6 @@
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="48" rx="8" fill="#1C1C1C"/>
<rect x="0.5" y="0.5" width="93" height="47" rx="7.5" stroke="white" stroke-opacity="0.24"/>
<rect x="8" y="8" width="78" height="12" rx="3" fill="white" fill-opacity="0.24"/>
<rect x="8" y="28" width="78" height="12" rx="3" fill="white" fill-opacity="0.48"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

@@ -0,0 +1,6 @@
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="28" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<rect x="8" y="8" width="35" height="12" rx="3" fill="black" fill-opacity="0.12"/>
<rect x="51" y="8" width="35" height="12" rx="3" fill="black" fill-opacity="0.32"/>
</svg>

After

Width:  |  Height:  |  Size: 414 B

@@ -0,0 +1,6 @@
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="28" rx="8" fill="#1C1C1C"/>
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="white" stroke-opacity="0.24"/>
<rect x="8" y="8" width="35" height="12" rx="3" fill="white" fill-opacity="0.24"/>
<rect x="51" y="8" width="35" height="12" rx="3" fill="white" fill-opacity="0.48"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="56" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.12"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M8 44C8 46.2091 9.79086 48 12 48H16C18.2091 48 20 46.2091 20 44C20 41.7909 18.2091 40 16 40H12C9.79086 40 8 41.7909 8 44Z" fill="black" fill-opacity="0.32"/>
<path d="M24.5 44C24.5 46.2091 26.2909 48 28.5 48H32.5C34.7091 48 36.5 46.2091 36.5 44C36.5 41.7909 34.7091 40 32.5 40H28.5C26.2909 40 24.5 41.7909 24.5 44Z" fill="black" fill-opacity="0.32"/>
<path d="M41 44C41 46.2091 42.7909 48 45 48H49C51.2091 48 53 46.2091 53 44C53 41.7909 51.2091 40 49 40H45C42.7909 40 41 41.7909 41 44Z" fill="black" fill-opacity="0.32"/>
<path d="M57.5 44C57.5 46.2091 59.2909 48 61.5 48H65.5C67.7091 48 69.5 46.2091 69.5 44C69.5 41.7909 67.7091 40 65.5 40H61.5C59.2909 40 57.5 41.7909 57.5 44Z" fill="black" fill-opacity="0.32"/>
<path d="M74 44C74 46.2091 75.7909 48 78 48H82C84.2091 48 86 46.2091 86 44C86 41.7909 84.2091 40 82 40H78C75.7909 40 74 41.7909 74 44Z" fill="black" fill-opacity="0.32"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V48C94 52.4183 90.4183 56 86 56H8C3.58172 56 0 52.4183 0 48V8Z" fill="#1C1C1C"/>
<path d="M1.34748 52.4449C0.772837 51.5866 0.359906 50.6109 0.152272 49.5613L0.642766 49.4643C0.549158 48.9911 0.5 48.5015 0.5 48V46H0V42H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.94999V0H13.85V0.5H17.75V0H21.65V0.5H25.55V0H29.45V0.5H33.35V0H37.25V0.5H41.15V0H45.05V0.5H48.95V0H52.85V0.5H56.75V0H60.65V0.5H64.55V0H68.45V0.5H72.35V0H76.25V0.5H80.15V0H84.05V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152273C88.6108 0.359907 89.5866 0.772837 90.4449 1.34747L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V34H94V38H93.5V42H94V46H93.5V48C93.5 48.5015 93.4508 48.9911 93.3572 49.4643L93.8477 49.5613C93.6401 50.6109 93.2272 51.5866 92.6525 52.4449L92.237 52.1668C91.6893 52.9849 90.9849 53.6893 90.1668 54.237L90.4449 54.6525C89.5866 55.2272 88.6108 55.6401 87.5613 55.8477L87.4643 55.3572C86.9911 55.4508 86.5015 55.5 86 55.5H84.05V56H80.15V55.5H76.25V56H72.35V55.5H68.45V56H64.55V55.5H60.65V56H56.75V55.5H52.85V56H48.95V55.5H45.05V56H41.15V55.5H37.25V56H33.35V55.5H29.45V56H25.55V55.5H21.65V56H17.75V55.5H13.85V56H9.95V55.5H8C7.49847 55.5 7.00892 55.4508 6.53574 55.3572L6.4387 55.8477C5.38915 55.6401 4.41341 55.2272 3.55508 54.6525L3.83323 54.237C3.01513 53.6893 2.31067 52.9849 1.76296 52.1668L1.34748 52.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="white" fill-opacity="0.24"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M8 44C8 46.2091 9.79086 48 12 48H16C18.2091 48 20 46.2091 20 44C20 41.7909 18.2091 40 16 40H12C9.79086 40 8 41.7909 8 44Z" fill="white" fill-opacity="0.48"/>
<path d="M24.5 44C24.5 46.2091 26.2909 48 28.5 48H32.5C34.7091 48 36.5 46.2091 36.5 44C36.5 41.7909 34.7091 40 32.5 40H28.5C26.2909 40 24.5 41.7909 24.5 44Z" fill="white" fill-opacity="0.48"/>
<path d="M41 44C41 46.2091 42.7909 48 45 48H49C51.2091 48 53 46.2091 53 44C53 41.7909 51.2091 40 49 40H45C42.7909 40 41 41.7909 41 44Z" fill="white" fill-opacity="0.48"/>
<path d="M57.5 44C57.5 46.2091 59.2909 48 61.5 48H65.5C67.7091 48 69.5 46.2091 69.5 44C69.5 41.7909 67.7091 40 65.5 40H61.5C59.2909 40 57.5 41.7909 57.5 44Z" fill="white" fill-opacity="0.48"/>
<path d="M74 44C74 46.2091 75.7909 48 78 48H82C84.2091 48 86 46.2091 86 44C86 41.7909 84.2091 40 82 40H78C75.7909 40 74 41.7909 74 44Z" fill="white" fill-opacity="0.48"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="56" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<path d="M8 12C8 14.2091 9.79086 16 12 16H16C18.2091 16 20 14.2091 20 12C20 9.79086 18.2091 8 16 8H12C9.79086 8 8 9.79086 8 12Z" fill="black" fill-opacity="0.32"/>
<path d="M24.5 12C24.5 14.2091 26.2909 16 28.5 16H32.5C34.7091 16 36.5 14.2091 36.5 12C36.5 9.79086 34.7091 8 32.5 8H28.5C26.2909 8 24.5 9.79086 24.5 12Z" fill="black" fill-opacity="0.32"/>
<path d="M41 12C41 14.2091 42.7909 16 45 16H49C51.2091 16 53 14.2091 53 12C53 9.79086 51.2091 8 49 8H45C42.7909 8 41 9.79086 41 12Z" fill="black" fill-opacity="0.32"/>
<path d="M57.5 12C57.5 14.2091 59.2909 16 61.5 16H65.5C67.7091 16 69.5 14.2091 69.5 12C69.5 9.79086 67.7091 8 65.5 8H61.5C59.2909 8 57.5 9.79086 57.5 12Z" fill="black" fill-opacity="0.32"/>
<path d="M74 12C74 14.2091 75.7909 16 78 16H82C84.2091 16 86 14.2091 86 12C86 9.79086 84.2091 8 82 8H78C75.7909 8 74 9.79086 74 12Z" fill="black" fill-opacity="0.32"/>
<path d="M8 30C8 26.6863 10.6863 24 14 24H33C36.3137 24 39 26.6863 39 30C39 33.3137 36.3137 36 33 36H14C10.6863 36 8 33.3137 8 30Z" fill="black" fill-opacity="0.12"/>
<path d="M8 43C8 41.3431 9.34315 40 11 40H83C84.6569 40 86 41.3431 86 43V45C86 46.6569 84.6569 48 83 48H11C9.34315 48 8 46.6569 8 45V43Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V48C94 52.4183 90.4183 56 86 56H8C3.58172 56 0 52.4183 0 48V8Z" fill="#1C1C1C"/>
<path d="M1.34748 52.4449C0.772837 51.5866 0.359906 50.6109 0.152272 49.5613L0.642766 49.4643C0.549158 48.9911 0.5 48.5015 0.5 48V46H0V42H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.94999V0H13.85V0.5H17.75V0H21.65V0.5H25.55V0H29.45V0.5H33.35V0H37.25V0.5H41.15V0H45.05V0.5H48.95V0H52.85V0.5H56.75V0H60.65V0.5H64.55V0H68.45V0.5H72.35V0H76.25V0.5H80.15V0H84.05V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152273C88.6108 0.359907 89.5866 0.772837 90.4449 1.34747L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V34H94V38H93.5V42H94V46H93.5V48C93.5 48.5015 93.4508 48.9911 93.3572 49.4643L93.8477 49.5613C93.6401 50.6109 93.2272 51.5866 92.6525 52.4449L92.237 52.1668C91.6893 52.9849 90.9849 53.6893 90.1668 54.237L90.4449 54.6525C89.5866 55.2272 88.6108 55.6401 87.5613 55.8477L87.4643 55.3572C86.9911 55.4508 86.5015 55.5 86 55.5H84.05V56H80.15V55.5H76.25V56H72.35V55.5H68.45V56H64.55V55.5H60.65V56H56.75V55.5H52.85V56H48.95V55.5H45.05V56H41.15V55.5H37.25V56H33.35V55.5H29.45V56H25.55V55.5H21.65V56H17.75V55.5H13.85V56H9.95V55.5H8C7.49847 55.5 7.00892 55.4508 6.53574 55.3572L6.4387 55.8477C5.38915 55.6401 4.41341 55.2272 3.55508 54.6525L3.83323 54.237C3.01513 53.6893 2.31067 52.9849 1.76296 52.1668L1.34748 52.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M8 12C8 14.2091 9.79086 16 12 16H16C18.2091 16 20 14.2091 20 12C20 9.79086 18.2091 8 16 8H12C9.79086 8 8 9.79086 8 12Z" fill="white" fill-opacity="0.48"/>
<path d="M24.5 12C24.5 14.2091 26.2909 16 28.5 16H32.5C34.7091 16 36.5 14.2091 36.5 12C36.5 9.79086 34.7091 8 32.5 8H28.5C26.2909 8 24.5 9.79086 24.5 12Z" fill="white" fill-opacity="0.48"/>
<path d="M41 12C41 14.2091 42.7909 16 45 16H49C51.2091 16 53 14.2091 53 12C53 9.79086 51.2091 8 49 8H45C42.7909 8 41 9.79086 41 12Z" fill="white" fill-opacity="0.48"/>
<path d="M57.5 12C57.5 14.2091 59.2909 16 61.5 16H65.5C67.7091 16 69.5 14.2091 69.5 12C69.5 9.79086 67.7091 8 65.5 8H61.5C59.2909 8 57.5 9.79086 57.5 12Z" fill="white" fill-opacity="0.48"/>
<path d="M74 12C74 14.2091 75.7909 16 78 16H82C84.2091 16 86 14.2091 86 12C86 9.79086 84.2091 8 82 8H78C75.7909 8 74 9.79086 74 12Z" fill="white" fill-opacity="0.48"/>
<path d="M8 30C8 26.6863 10.6863 24 14 24H33C36.3137 24 39 26.6863 39 30C39 33.3137 36.3137 36 33 36H14C10.6863 36 8 33.3137 8 30Z" fill="white" fill-opacity="0.24"/>
<path d="M8 43C8 41.3431 9.34315 40 11 40H83C84.6569 40 86 41.3431 86 43V45C86 46.6569 84.6569 48 83 48H11C9.34315 48 8 46.6569 8 45V43Z" fill="white" fill-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="56" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<path d="M31.5 14C31.5 10.6863 34.1863 8 37.5 8H56.5C59.8137 8 62.5 10.6863 62.5 14C62.5 17.3137 59.8137 20 56.5 20H37.5C34.1863 20 31.5 17.3137 31.5 14Z" fill="black" fill-opacity="0.32"/>
<path d="M23 27C23 25.3431 24.3431 24 26 24H68C69.6569 24 71 25.3431 71 27V29C71 30.6569 69.6569 32 68 32H26C24.3431 32 23 30.6569 23 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M9 44C9 41.7909 10.7909 40 13 40H17C19.2091 40 21 41.7909 21 44C21 46.2091 19.2091 48 17 48H13C10.7909 48 9 46.2091 9 44Z" fill="black" fill-opacity="0.32"/>
<path d="M25 44C25 41.7909 26.7909 40 29 40H33C35.2091 40 37 41.7909 37 44C37 46.2091 35.2091 48 33 48H29C26.7909 48 25 46.2091 25 44Z" fill="black" fill-opacity="0.12"/>
<path d="M41 44C41 41.7909 42.7909 40 45 40H49C51.2091 40 53 41.7909 53 44C53 46.2091 51.2091 48 49 48H45C42.7909 48 41 46.2091 41 44Z" fill="black" fill-opacity="0.12"/>
<path d="M57 44C57 41.7909 58.7909 40 61 40H65C67.2091 40 69 41.7909 69 44C69 46.2091 67.2091 48 65 48H61C58.7909 48 57 46.2091 57 44Z" fill="black" fill-opacity="0.12"/>
<path d="M73 44C73 41.7909 74.7909 40 77 40H81C83.2091 40 85 41.7909 85 44C85 46.2091 83.2091 48 81 48H77C74.7909 48 73 46.2091 73 44Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V48C94 52.4183 90.4183 56 86 56H8C3.58172 56 0 52.4183 0 48V8Z" fill="#1C1C1C"/>
<path d="M1.34748 52.4449C0.772837 51.5866 0.359906 50.6109 0.152272 49.5613L0.642766 49.4643C0.549158 48.9911 0.5 48.5015 0.5 48V46H0V42H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.94999V0H13.85V0.5H17.75V0H21.65V0.5H25.55V0H29.45V0.5H33.35V0H37.25V0.5H41.15V0H45.05V0.5H48.95V0H52.85V0.5H56.75V0H60.65V0.5H64.55V0H68.45V0.5H72.35V0H76.25V0.5H80.15V0H84.05V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152273C88.6108 0.359907 89.5866 0.772837 90.4449 1.34747L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V34H94V38H93.5V42H94V46H93.5V48C93.5 48.5015 93.4508 48.9911 93.3572 49.4643L93.8477 49.5613C93.6401 50.6109 93.2272 51.5866 92.6525 52.4449L92.237 52.1668C91.6893 52.9849 90.9849 53.6893 90.1668 54.237L90.4449 54.6525C89.5866 55.2272 88.6108 55.6401 87.5613 55.8477L87.4643 55.3572C86.9911 55.4508 86.5015 55.5 86 55.5H84.05V56H80.15V55.5H76.25V56H72.35V55.5H68.45V56H64.55V55.5H60.65V56H56.75V55.5H52.85V56H48.95V55.5H45.05V56H41.15V55.5H37.25V56H33.35V55.5H29.45V56H25.55V55.5H21.65V56H17.75V55.5H13.85V56H9.95V55.5H8C7.49847 55.5 7.00892 55.4508 6.53574 55.3572L6.4387 55.8477C5.38915 55.6401 4.41341 55.2272 3.55508 54.6525L3.83323 54.237C3.01513 53.6893 2.31067 52.9849 1.76296 52.1668L1.34748 52.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M31.5 14C31.5 10.6863 34.1863 8 37.5 8H56.5C59.8137 8 62.5 10.6863 62.5 14C62.5 17.3137 59.8137 20 56.5 20H37.5C34.1863 20 31.5 17.3137 31.5 14Z" fill="white" fill-opacity="0.48"/>
<path d="M23 27C23 25.3431 24.3431 24 26 24H68C69.6569 24 71 25.3431 71 27V29C71 30.6569 69.6569 32 68 32H26C24.3431 32 23 30.6569 23 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M9 44C9 41.7909 10.7909 40 13 40H17C19.2091 40 21 41.7909 21 44C21 46.2091 19.2091 48 17 48H13C10.7909 48 9 46.2091 9 44Z" fill="white" fill-opacity="0.48"/>
<rect x="25" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
<rect x="41" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
<rect x="57" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
<rect x="73" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,24 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_887_2968)">
<path d="M0 39H55V64C55 68.4183 51.4183 72 47 72H8C3.58172 72 0 68.4183 0 64V39Z" fill="white"/>
<path d="M1.34748 68.4449C0.772837 67.5866 0.359906 66.6109 0.152272 65.5613L0.642766 65.4643C0.549158 64.9911 0.5 64.5015 0.5 64V61.9167H0V57.75H0.5V53.5833H0V49.4167H0.5V45.25H0V41.0833H0.5V39.5H1.96429V39H5.89286V39.5H9.82143V39H13.75V39.5H17.6786V39H21.6071V39.5H25.5357V39H29.4643V39.5H33.3929V39H37.3214V39.5H41.25V39H45.1786V39.5H49.1071V39H53.0357V39.5H54.5V41.0833H55V45.25H54.5V49.4167H55V53.5833H54.5V57.75H55V61.9167H54.5V64C54.5 64.5015 54.4508 64.9911 54.3572 65.4643L54.8477 65.5613C54.6401 66.6109 54.2272 67.5866 53.6525 68.4449L53.237 68.1668C52.6893 68.9849 51.9849 69.6893 51.1668 70.237L51.4449 70.6525C50.5866 71.2272 49.6109 71.6401 48.5613 71.8477L48.4643 71.3572C47.9911 71.4508 47.5015 71.5 47 71.5H45.05V72H41.15V71.5H37.25V72H33.35V71.5H29.45V72H25.55V71.5H21.65V72H17.75V71.5H13.85V72H9.95V71.5H8C7.49847 71.5 7.00892 71.4508 6.53574 71.3572L6.4387 71.8477C5.38915 71.6401 4.41341 71.2272 3.55507 70.6525L3.83323 70.237C3.01513 69.6893 2.31067 68.9849 1.76296 68.1668L1.34748 68.4449Z" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<rect x="8" y="47" width="12" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<rect x="24" y="47" width="12" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<rect x="8" y="59" width="12" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M54 0H86C90.4183 0 94 3.58172 94 8V32C94 36.4183 90.4183 40 86 40H54V0Z" fill="white"/>
<path d="M84 39.5V40H80V39.5H76V40H72V39.5H68V40H64V39.5H60V40H56V39.5H54.5V38H54V34H54.5V30H54V26H54.5V22H54V18H54.5V14H54V10H54.5V6H54V2H54.5V0.5H56V0H60V0.5H64V0H68V0.5H72V0H76V0.5H80V0H84V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152272C88.6109 0.359906 89.5866 0.772836 90.4449 1.34748L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V32C93.5 32.5015 93.4508 32.9911 93.3572 33.4643L93.8477 33.5613C93.6401 34.6109 93.2272 35.5866 92.6525 36.4449L92.237 36.1668C91.6893 36.9849 90.9849 37.6893 90.1668 38.237L90.4449 38.6525C89.5866 39.2272 88.6109 39.6401 87.5613 39.8477L87.4643 39.3572C86.9911 39.4508 86.5015 39.5 86 39.5H84Z" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<path d="M58 28C58 30.2091 59.7909 32 62 32H66C68.2091 32 70 30.2091 70 28C70 25.7909 68.2091 24 66 24H62C59.7909 24 58 25.7909 58 28Z" fill="black" fill-opacity="0.12"/>
<path d="M74 28C74 30.2091 75.7909 32 78 32H82C84.2091 32 86 30.2091 86 28C86 25.7909 84.2091 24 82 24H78C75.7909 24 74 25.7909 74 28Z" fill="black" fill-opacity="0.12"/>
<path d="M74 16C74 18.2091 75.7909 20 78 20H82C84.2091 20 86 18.2091 86 16C86 13.7909 84.2091 12 82 12H78C75.7909 12 74 13.7909 74 16Z" fill="black" fill-opacity="0.32"/>
<path d="M0 8C0 3.58172 3.58172 0 8 0H55V40H0V8Z" fill="white"/>
<path d="M3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.95833V0H13.875V0.5H17.7917V0H21.7083V0.5H25.625V0H29.5417V0.5H33.4583V0H37.375V0.5H41.2917V0H45.2083V0.5H49.125V0H53.0417V0.5H54.5V2H55V6H54.5V10H55V14H54.5V18H55V22H54.5V26H55V30H54.5V34H55V38H54.5V39.5H53.0357V40H49.1071V39.5H45.1786V40H41.25V39.5H37.3214V40H33.3929V39.5H29.4643V40H25.5357V39.5H21.6071V40H17.6786V39.5H13.75V40H9.82143V39.5H5.89286V40H1.96429V39.5H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748Z" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H44C45.6569 24 47 25.3431 47 27V29C47 30.6569 45.6569 32 44 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M79 48V54.5C79 58.09 76.09 61 72.5 61H66.83L69.92 64.09L68.5 65.5L63 60L68.5 54.5L69.91 55.91L66.83 59H72.5C75 59 77 57 77 54.5V48H79Z" fill="black" fill-opacity="0.32"/>
</g>
<defs>
<clipPath id="clip0_887_2968">
<rect width="94" height="72" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

@@ -0,0 +1,17 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 39H55V64C55 68.4183 51.4183 72 47 72H8C3.58172 72 0 68.4183 0 64V39Z" fill="#1C1C1C"/>
<path d="M1.34748 68.4449C0.772837 67.5866 0.359906 66.6109 0.152272 65.5613L0.642766 65.4643C0.549158 64.9911 0.5 64.5015 0.5 64V61.9167H0V57.75H0.5V53.5833H0V49.4167H0.5V45.25H0V41.0833H0.5V39.5H1.96429V39H5.89286V39.5H9.82143V39H13.75V39.5H17.6786V39H21.6071V39.5H25.5357V39H29.4643V39.5H33.3929V39H37.3214V39.5H41.25V39H45.1786V39.5H49.1071V39H53.0357V39.5H54.5V41.0833H55V45.25H54.5V49.4167H55V53.5833H54.5V57.75H55V61.9167H54.5V64C54.5 64.5015 54.4508 64.9911 54.3572 65.4643L54.8477 65.5613C54.6401 66.6109 54.2272 67.5866 53.6525 68.4449L53.237 68.1668C52.6893 68.9849 51.9849 69.6893 51.1668 70.237L51.4449 70.6525C50.5866 71.2272 49.6109 71.6401 48.5613 71.8477L48.4643 71.3572C47.9911 71.4508 47.5015 71.5 47 71.5H45.05V72H41.15V71.5H37.25V72H33.35V71.5H29.45V72H25.55V71.5H21.65V72H17.75V71.5H13.85V72H9.95V71.5H8C7.49847 71.5 7.00892 71.4508 6.53574 71.3572L6.4387 71.8477C5.38915 71.6401 4.41341 71.2272 3.55507 70.6525L3.83323 70.237C3.01513 69.6893 2.31067 68.9849 1.76296 68.1668L1.34748 68.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M8 51C8 48.7909 9.79086 47 12 47H16C18.2091 47 20 48.7909 20 51C20 53.2091 18.2091 55 16 55H12C9.79086 55 8 53.2091 8 51Z" fill="white" fill-opacity="0.48"/>
<path d="M24 51C24 48.7909 25.7909 47 28 47H32C34.2091 47 36 48.7909 36 51C36 53.2091 34.2091 55 32 55H28C25.7909 55 24 53.2091 24 51Z" fill="white" fill-opacity="0.24"/>
<path d="M8 63C8 60.7909 9.79086 59 12 59H16C18.2091 59 20 60.7909 20 63C20 65.2091 18.2091 67 16 67H12C9.79086 67 8 65.2091 8 63Z" fill="white" fill-opacity="0.24"/>
<path d="M54 0H86C90.4183 0 94 3.58172 94 8V32C94 36.4183 90.4183 40 86 40H54V0Z" fill="#1C1C1C"/>
<path d="M84 39.5V40H80V39.5H76V40H72V39.5H68V40H64V39.5H60V40H56V39.5H54.5V38H54V34H54.5V30H54V26H54.5V22H54V18H54.5V14H54V10H54.5V6H54V2H54.5V0.5H56V0H60V0.5H64V0H68V0.5H72V0H76V0.5H80V0H84V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152272C88.6109 0.359906 89.5866 0.772836 90.4449 1.34748L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V32C93.5 32.5015 93.4508 32.9911 93.3572 33.4643L93.8477 33.5613C93.6401 34.6109 93.2272 35.5866 92.6525 36.4449L92.237 36.1668C91.6893 36.9849 90.9849 37.6893 90.1668 38.237L90.4449 38.6525C89.5866 39.2272 88.6109 39.6401 87.5613 39.8477L87.4643 39.3572C86.9911 39.4508 86.5015 39.5 86 39.5H84Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M58 28C58 30.2091 59.7909 32 62 32H66C68.2091 32 70 30.2091 70 28C70 25.7909 68.2091 24 66 24H62C59.7909 24 58 25.7909 58 28Z" fill="white" fill-opacity="0.24"/>
<path d="M74 28C74 30.2091 75.7909 32 78 32H82C84.2091 32 86 30.2091 86 28C86 25.7909 84.2091 24 82 24H78C75.7909 24 74 25.7909 74 28Z" fill="white" fill-opacity="0.24"/>
<path d="M74 16C74 18.2091 75.7909 20 78 20H82C84.2091 20 86 18.2091 86 16C86 13.7909 84.2091 12 82 12H78C75.7909 12 74 13.7909 74 16Z" fill="white" fill-opacity="0.48"/>
<path d="M0 8C0 3.58172 3.58172 0 8 0H55V40H0V8Z" fill="#1C1C1C"/>
<path d="M3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.95833V0H13.875V0.5H17.7917V0H21.7083V0.5H25.625V0H29.5417V0.5H33.4583V0H37.375V0.5H41.2917V0H45.2083V0.5H49.125V0H53.0417V0.5H54.5V2H55V6H54.5V10H55V14H54.5V18H55V22H54.5V26H55V30H54.5V34H55V38H54.5V39.5H53.0357V40H49.1071V39.5H45.1786V40H41.25V39.5H37.3214V40H33.3929V39.5H29.4643V40H25.5357V39.5H21.6071V40H17.6786V39.5H13.75V40H9.82143V39.5H5.89286V40H1.96429V39.5H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="white" fill-opacity="0.48"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H44C45.6569 24 47 25.3431 47 27V29C47 30.6569 45.6569 32 44 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M79 48V54.5C79 58.09 76.09 61 72.5 61H66.83L69.92 64.09L68.5 65.5L63 60L68.5 54.5L69.91 55.91L66.83 59H72.5C75 59 77 57 77 54.5V48H79Z" fill="white" fill-opacity="0.48"/>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,9 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="56" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H53C54.6569 24 56 25.3431 56 27V29C56 30.6569 54.6569 32 53 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M8 44C8 41.7909 9.79086 40 12 40H16C18.2091 40 20 41.7909 20 44C20 46.2091 18.2091 48 16 48H12C9.79086 48 8 46.2091 8 44Z" fill="black" fill-opacity="0.32"/>
<path d="M24 44C24 41.7909 25.7909 40 28 40H32C34.2091 40 36 41.7909 36 44C36 46.2091 34.2091 48 32 48H28C25.7909 48 24 46.2091 24 44Z" fill="black" fill-opacity="0.12"/>
<path d="M40 44C40 41.7909 41.7909 40 44 40H48C50.2091 40 52 41.7909 52 44C52 46.2091 50.2091 48 48 48H44C41.7909 48 40 46.2091 40 44Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V48C94 52.4183 90.4183 56 86 56H8C3.58172 56 0 52.4183 0 48V8Z" fill="#1C1C1C"/>
<path d="M1.34748 52.4449C0.772837 51.5866 0.359906 50.6109 0.152272 49.5613L0.642766 49.4643C0.549158 48.9911 0.5 48.5015 0.5 48V46H0V42H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.94999V0H13.85V0.5H17.75V0H21.65V0.5H25.55V0H29.45V0.5H33.35V0H37.25V0.5H41.15V0H45.05V0.5H48.95V0H52.85V0.5H56.75V0H60.65V0.5H64.55V0H68.45V0.5H72.35V0H76.25V0.5H80.15V0H84.05V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152273C88.6108 0.359907 89.5866 0.772837 90.4449 1.34747L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V34H94V38H93.5V42H94V46H93.5V48C93.5 48.5015 93.4508 48.9911 93.3572 49.4643L93.8477 49.5613C93.6401 50.6109 93.2272 51.5866 92.6525 52.4449L92.237 52.1668C91.6893 52.9849 90.9849 53.6893 90.1668 54.237L90.4449 54.6525C89.5866 55.2272 88.6108 55.6401 87.5613 55.8477L87.4643 55.3572C86.9911 55.4508 86.5015 55.5 86 55.5H84.05V56H80.15V55.5H76.25V56H72.35V55.5H68.45V56H64.55V55.5H60.65V56H56.75V55.5H52.85V56H48.95V55.5H45.05V56H41.15V55.5H37.25V56H33.35V55.5H29.45V56H25.55V55.5H21.65V56H17.75V55.5H13.85V56H9.95V55.5H8C7.49847 55.5 7.00892 55.4508 6.53574 55.3572L6.4387 55.8477C5.38915 55.6401 4.41341 55.2272 3.55508 54.6525L3.83323 54.237C3.01513 53.6893 2.31067 52.9849 1.76296 52.1668L1.34748 52.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="white" fill-opacity="0.48"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H53C54.6569 24 56 25.3431 56 27V29C56 30.6569 54.6569 32 53 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M8 44C8 41.7909 9.79086 40 12 40H16C18.2091 40 20 41.7909 20 44C20 46.2091 18.2091 48 16 48H12C9.79086 48 8 46.2091 8 44Z" fill="white" fill-opacity="0.48"/>
<rect x="24" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
<rect x="40" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
<rect x="56" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
<rect x="72" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

-8
View File
@@ -32,14 +32,6 @@ export const setupLeafletMap = async (
markerClusterStyle.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(markerClusterStyle);
const defaultMarkerClusterStyle = document.createElement("link");
defaultMarkerClusterStyle.setAttribute(
"href",
"/static/images/leaflet/MarkerCluster.Default.css"
);
defaultMarkerClusterStyle.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(defaultMarkerClusterStyle);
map.setView([52.3731339, 4.8903147], 13);
const tileLayer = createTileLayer(Leaflet).addTo(map);
+1 -1
View File
@@ -1,2 +1,2 @@
export const computeDomain = (entityId: string): string =>
entityId.substr(0, entityId.indexOf("."));
entityId.substring(0, entityId.indexOf("."));
+1 -5
View File
@@ -120,11 +120,6 @@ export const computeStateDisplayFromEntityAttributes = (
return value;
}
if (domain === "datetime") {
const time = new Date(state);
return formatDateTime(time, locale, config);
}
if (["date", "input_datetime", "time"].includes(domain)) {
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
// Attributes aren't available, we have to use `state`.
@@ -181,6 +176,7 @@ export const computeStateDisplayFromEntityAttributes = (
"tag",
"tts",
"wake_word",
"datetime",
].includes(domain) ||
(domain === "sensor" && attributes.device_class === "timestamp")
) {
+294 -114
View File
@@ -1,11 +1,12 @@
import { consume } from "@lit-labs/context";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { mdiRestart } from "@mdi/js";
import { mdiChevronDown, mdiChevronUp, mdiRestart } from "@mdi/js";
import { differenceInMinutes } from "date-fns";
import type { DataZoomComponentOption } from "echarts/components";
import type { EChartsType } from "echarts/core";
import type {
ECElementEvent,
LegendComponentOption,
XAXisOption,
YAXisOption,
} from "echarts/types/dist/shared";
@@ -25,8 +26,12 @@ import { isMac } from "../../util/is_mac";
import "../ha-icon-button";
import { formatTimeLabel } from "./axis-label";
import { ensureArray } from "../../common/array/ensure-array";
import "../chips/ha-assist-chip";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
const LEGEND_OVERFLOW_LIMIT = 10;
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
const DOUBLE_TAP_TIME = 300;
@customElement("ha-chart-base")
export class HaChartBase extends LitElement {
@@ -40,8 +45,10 @@ export class HaChartBase extends LitElement {
@property({ type: String }) public height?: string;
@property({ attribute: "external-hidden", type: Boolean })
public externalHidden = false;
@property({ attribute: "expand-legend", type: Boolean })
public expandLegend?: boolean;
@property({ attribute: false }) public extraComponents?: any[];
@state()
@consume({ context: themesContext, subscribe: true })
@@ -53,10 +60,14 @@ export class HaChartBase extends LitElement {
@state() private _minutesDifference = 24 * 60;
@state() private _hiddenDatasets = new Set<string>();
private _modifierPressed = false;
private _isTouchDevice = "ontouchstart" in window;
private _lastTapTime?: number;
// @ts-ignore
private _resizeController = new ResizeController(this, {
callback: () => this.chart?.resize(),
@@ -102,6 +113,12 @@ export class HaChartBase extends LitElement {
if (!this.options?.dataZoom) {
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
}
// drag to zoom
this.chart?.dispatchAction({
type: "takeGlobalCursor",
key: "dataZoomSelect",
dataZoomSelectActive: true,
});
}
};
@@ -111,6 +128,11 @@ export class HaChartBase extends LitElement {
if (!this.options?.dataZoom) {
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
}
this.chart?.dispatchAction({
type: "takeGlobalCursor",
key: "dataZoomSelect",
dataZoomSelectActive: false,
});
}
};
@@ -135,8 +157,8 @@ export class HaChartBase extends LitElement {
return;
}
let chartOptions: ECOption = {};
if (changedProps.has("data")) {
chartOptions.series = this.data;
if (changedProps.has("data") || changedProps.has("_hiddenDatasets")) {
chartOptions.series = this._getSeries();
}
if (changedProps.has("options")) {
chartOptions = { ...chartOptions, ...this._createOptions() };
@@ -151,15 +173,18 @@ export class HaChartBase extends LitElement {
protected render() {
return html`
<div
class=${classMap({
"chart-container": true,
"has-legend": !!this.options?.legend,
})}
style=${styleMap({
height: this.height ?? `${this._getDefaultHeight()}px`,
})}
class="container ${classMap({ "has-height": !!this.height })}"
style=${styleMap({ height: this.height })}
>
<div class="chart"></div>
<div
class="chart-container"
style=${styleMap({
height: this.height ? undefined : `${this._getDefaultHeight()}px`,
})}
>
<div class="chart"></div>
</div>
${this._renderLegend()}
${this._isZoomed
? html`<ha-icon-button
class="zoom-reset"
@@ -174,6 +199,74 @@ export class HaChartBase extends LitElement {
`;
}
private _renderLegend() {
if (!this.options?.legend || !this.data) {
return nothing;
}
const legend = ensureArray(this.options.legend)[0] as LegendComponentOption;
if (!legend.show) {
return nothing;
}
const datasets = ensureArray(this.data);
const items = (legend.data ||
datasets
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
.map((d) => d.name ?? d.id) ||
[]) as string[];
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
const overflowLimit = isMobile
? LEGEND_OVERFLOW_LIMIT_MOBILE
: LEGEND_OVERFLOW_LIMIT;
return html`<div class="chart-legend">
<ul>
${items.map((item: string, index: number) => {
if (!this.expandLegend && index >= overflowLimit) {
return nothing;
}
const dataset = datasets.find(
(d) => d.id === item || d.name === item
);
const color = dataset?.color as string;
const borderColor = dataset?.itemStyle?.borderColor as string;
return html`<li
.name=${item}
@click=${this._legendClick}
class=${classMap({ hidden: this._hiddenDatasets.has(item) })}
.title=${item}
>
<div
class="bullet"
style=${styleMap({
backgroundColor: color,
borderColor: borderColor || color,
})}
></div>
<div class="label">${item}</div>
</li>`;
})}
${items.length > overflowLimit
? html`<li>
<ha-assist-chip
@click=${this._toggleExpandedLegend}
filled
label=${`${this.hass.localize(
`ui.components.history_charts.${this.expandLegend ? "collapse_legend" : "expand_legend"}`
)} (${items.length})`}
>
<ha-svg-icon
slot="trailing-icon"
.path=${this.expandLegend ? mdiChevronUp : mdiChevronDown}
></ha-svg-icon>
</ha-assist-chip>
</li>`
: nothing}
</ul>
</div>`;
}
private _formatTimeLabel = (value: number | Date) =>
formatTimeLabel(
value,
@@ -192,19 +285,13 @@ export class HaChartBase extends LitElement {
}
const echarts = (await import("../../resources/echarts")).default;
if (this.extraComponents?.length) {
echarts.use(this.extraComponents);
}
echarts.registerTheme("custom", this._createTheme());
this.chart = echarts.init(container, "custom");
this.chart.on("legendselectchanged", (params: any) => {
if (this.externalHidden) {
const isSelected = params.selected[params.name];
if (isSelected) {
fireEvent(this, "dataset-unhidden", { name: params.name });
} else {
fireEvent(this, "dataset-hidden", { name: params.name });
}
}
});
this.chart.on("datazoom", (e: any) => {
const { start, end } = e.batch?.[0] ?? e;
this._isZoomed = start !== 0 || end !== 100;
@@ -213,24 +300,39 @@ export class HaChartBase extends LitElement {
this.chart.on("click", (e: ECElementEvent) => {
fireEvent(this, "chart-click", e);
});
this.chart.on("mousemove", (e: ECElementEvent) => {
if (e.componentType === "series" && e.componentSubType === "custom") {
// custom series do not support cursor style so we need to set it manually
this.chart?.getZr()?.setCursorStyle("default");
}
this.chart.getZr().on("dblclick", this._handleClickZoom);
if (this._isTouchDevice) {
this.chart.getZr().on("click", (e: ECElementEvent) => {
if (!e.zrByTouch) {
return;
}
if (
this._lastTapTime &&
Date.now() - this._lastTapTime < DOUBLE_TAP_TIME
) {
this._handleClickZoom(e);
} else {
this._lastTapTime = Date.now();
}
});
}
this.chart.setOption({
...this._createOptions(),
series: this._getSeries(),
});
this.chart.setOption({ ...this._createOptions(), series: this.data });
} finally {
this._loading = false;
}
}
private _getDataZoomConfig(): DataZoomComponentOption | undefined {
const xAxis = (this.options?.xAxis?.[0] ??
this.options?.xAxis) as XAXisOption;
const yAxis = (this.options?.yAxis?.[0] ??
this.options?.yAxis) as YAXisOption;
if (xAxis.type === "value" && yAxis.type === "category") {
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
| XAXisOption
| undefined;
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
| YAXisOption
| undefined;
if (xAxis?.type === "value" && yAxis?.type === "category") {
// vertical data zoom doesn't work well in this case and horizontal is pointless
return undefined;
}
@@ -270,20 +372,12 @@ export class HaChartBase extends LitElement {
: undefined;
}
return {
axisLine: {
show: false,
},
splitLine: {
show: true,
},
axisLine: { show: false },
splitLine: { show: true },
...axis,
axisLabel: {
formatter: this._formatTimeLabel,
rich: {
bold: {
fontWeight: "bold",
},
},
rich: { bold: { fontWeight: "bold" } },
hideOverlap: true,
...axis.axisLabel,
},
@@ -294,11 +388,18 @@ export class HaChartBase extends LitElement {
const options = {
animation: !this._reducedMotion,
darkMode: this._themes.darkMode ?? false,
aria: {
show: true,
},
aria: { show: true },
dataZoom: this._getDataZoomConfig(),
toolbox: {
top: Infinity,
left: Infinity,
feature: {
dataZoom: { show: true, yAxisIndex: false, filterMode: "none" },
},
iconStyle: { opacity: 0 },
},
...this.options,
legend: { show: false },
xAxis,
};
@@ -330,42 +431,28 @@ export class HaChartBase extends LitElement {
fontFamily: "Roboto, Noto, sans-serif",
},
title: {
textStyle: {
color: style.getPropertyValue("--primary-text-color"),
},
textStyle: { color: style.getPropertyValue("--primary-text-color") },
subtextStyle: {
color: style.getPropertyValue("--secondary-text-color"),
},
},
line: {
lineStyle: {
width: 1.5,
},
lineStyle: { width: 1.5 },
symbolSize: 1,
symbol: "circle",
smooth: false,
},
bar: {
itemStyle: {
barBorderWidth: 1.5,
},
},
bar: { itemStyle: { barBorderWidth: 1.5 } },
categoryAxis: {
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
show: true,
color: style.getPropertyValue("--primary-text-color"),
},
splitLine: {
show: false,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
lineStyle: { color: style.getPropertyValue("--divider-color") },
},
splitArea: {
show: false,
@@ -380,15 +467,11 @@ export class HaChartBase extends LitElement {
valueAxis: {
axisLine: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
lineStyle: { color: style.getPropertyValue("--divider-color") },
},
axisTick: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
lineStyle: { color: style.getPropertyValue("--divider-color") },
},
axisLabel: {
show: true,
@@ -396,9 +479,7 @@ export class HaChartBase extends LitElement {
},
splitLine: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
lineStyle: { color: style.getPropertyValue("--divider-color") },
},
splitArea: {
show: false,
@@ -413,15 +494,11 @@ export class HaChartBase extends LitElement {
logAxis: {
axisLine: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
lineStyle: { color: style.getPropertyValue("--divider-color") },
},
axisTick: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
lineStyle: { color: style.getPropertyValue("--divider-color") },
},
axisLabel: {
show: true,
@@ -429,9 +506,7 @@ export class HaChartBase extends LitElement {
},
splitLine: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
lineStyle: { color: style.getPropertyValue("--divider-color") },
},
splitArea: {
show: false,
@@ -446,15 +521,11 @@ export class HaChartBase extends LitElement {
timeAxis: {
axisLine: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
lineStyle: { color: style.getPropertyValue("--divider-color") },
},
axisTick: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
lineStyle: { color: style.getPropertyValue("--divider-color") },
},
axisLabel: {
show: true,
@@ -462,9 +533,7 @@ export class HaChartBase extends LitElement {
},
splitLine: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
lineStyle: { color: style.getPropertyValue("--divider-color") },
},
splitArea: {
show: false,
@@ -477,9 +546,7 @@ export class HaChartBase extends LitElement {
},
},
legend: {
textStyle: {
color: style.getPropertyValue("--primary-text-color"),
},
textStyle: { color: style.getPropertyValue("--primary-text-color") },
inactiveColor: style.getPropertyValue("--disabled-text-color"),
pageIconColor: style.getPropertyValue("--primary-text-color"),
pageIconInactiveColor: style.getPropertyValue("--disabled-text-color"),
@@ -495,18 +562,23 @@ export class HaChartBase extends LitElement {
fontSize: 12,
},
axisPointer: {
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
crossStyle: {
color: style.getPropertyValue("--divider-color"),
},
lineStyle: { color: style.getPropertyValue("--divider-color") },
crossStyle: { color: style.getPropertyValue("--divider-color") },
},
},
timeline: {},
};
}
private _getSeries() {
if (!Array.isArray(this.data)) {
return this.data;
}
return this.data.filter(
(d) => !this._hiddenDatasets.has(String(d.name ?? d.id))
);
}
private _getDefaultHeight() {
return Math.max(this.clientWidth / 2, 200);
}
@@ -517,18 +589,18 @@ export class HaChartBase extends LitElement {
}
if (!this._originalZrFlush) {
const dataSize = ensureArray(this.data).reduce(
(acc, series) => acc + (series.data as any[]).length,
(acc, series) => acc + ((series.data as any[]) || []).length,
0
);
if (dataSize > 10000) {
// for large datasets zr.flush takes 30-40% of the render time
// so we delay it a bit to avoid blocking the main thread
// delay the last bit of the render to avoid blocking the main thread
// this is not that impactful with sampling enabled but it doesn't hurt to have it
const zr = this.chart.getZr();
this._originalZrFlush = zr.flush.bind(zr);
this._originalZrFlush = zr.flush;
zr.flush = () => {
setTimeout(() => {
this._originalZrFlush?.();
}, 10);
this._originalZrFlush?.call(zr);
}, 5);
};
}
}
@@ -536,23 +608,73 @@ export class HaChartBase extends LitElement {
this.chart.setOption(options, { replaceMerge });
}
private _handleClickZoom = (e: ECElementEvent) => {
if (!this.chart) {
return;
}
const range = this._isZoomed
? [0, 100]
: [
(e.offsetX / this.chart.getWidth()) * 100 - 15,
(e.offsetX / this.chart.getWidth()) * 100 + 15,
];
this.chart.dispatchAction({
type: "dataZoom",
start: range[0],
end: range[1],
});
};
private _handleZoomReset() {
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
}
private _legendClick(ev: any) {
if (!this.chart) {
return;
}
const name = ev.currentTarget?.name;
if (this._hiddenDatasets.has(name)) {
this._hiddenDatasets.delete(name);
fireEvent(this, "dataset-unhidden", { name });
} else {
this._hiddenDatasets.add(name);
fireEvent(this, "dataset-hidden", { name });
}
this.requestUpdate("_hiddenDatasets");
}
private _toggleExpandedLegend() {
this.expandLegend = !this.expandLegend;
setTimeout(() => {
this.chart?.resize();
});
}
static styles = css`
:host {
display: block;
position: relative;
letter-spacing: normal;
}
.chart-container {
.container {
display: flex;
flex-direction: column;
position: relative;
}
.container.has-height {
max-height: var(--chart-max-height, 350px);
}
.chart {
.chart-container {
width: 100%;
max-height: var(--chart-max-height, 350px);
}
.has-height .chart-container {
flex: 1;
}
.chart {
height: 100%;
width: 100%;
}
.zoom-reset {
position: absolute;
@@ -564,8 +686,66 @@ export class HaChartBase extends LitElement {
color: var(--primary-color);
border: 1px solid var(--divider-color);
}
.has-legend .zoom-reset {
top: 64px;
.chart-legend {
max-height: 60%;
overflow-y: auto;
margin: 12px 0 0;
font-size: 12px;
color: var(--primary-text-color);
}
.chart-legend ul {
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 8px;
}
.chart-legend li {
height: 24px;
cursor: pointer;
display: inline-flex;
align-items: center;
padding: 0 2px;
box-sizing: border-box;
max-width: 220px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.chart-legend .hidden {
color: var(--secondary-text-color);
}
.chart-legend .label {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.chart-legend .bullet {
border-width: 1px;
border-style: solid;
border-radius: 50%;
display: block;
height: 16px;
width: 16px;
margin-right: 4px;
flex-shrink: 0;
box-sizing: border-box;
margin-inline-end: 4px;
margin-inline-start: initial;
direction: var(--direction);
}
.chart-legend .hidden .bullet {
border-color: var(--secondary-text-color) !important;
background-color: transparent !important;
}
ha-assist-chip {
height: 100%;
--_label-text-weight: 500;
--_leading-space: 8px;
--_trailing-space: 8px;
--_icon-label-space: 4px;
}
`;
}
+143 -434
View File
@@ -1,9 +1,17 @@
import { customElement, property } from "lit/decorators";
import { LitElement, html, css, svg, nothing } from "lit";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { customElement, property, state } from "lit/decorators";
import { LitElement, html, css } from "lit";
import type { EChartsType } from "echarts/core";
import type { CallbackDataParams } from "echarts/types/dist/shared";
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
import { SankeyChart } from "echarts/charts";
import memoizeOne from "memoize-one";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import type { HomeAssistant } from "../../types";
import type { ECOption } from "../../resources/echarts";
import { measureTextWidth } from "../../util/text";
import "./ha-chart-base";
import { NODE_SIZE } from "../trace/hat-graph-const";
import "../ha-alert";
export interface Node {
id: string;
@@ -25,34 +33,14 @@ export interface SankeyChartData {
links: Link[];
}
type ProcessedNode = Node & {
x: number;
y: number;
size: number;
};
type ProcessedLink = Link & {
value: number;
offset: {
source: number;
target: number;
};
passThroughNodeIds: string[];
};
interface Section {
nodes: ProcessedNode[];
offset: number;
index: number;
totalValue: number;
statePerPixel: number;
}
const MIN_SIZE = 3;
const DEFAULT_COLOR = "var(--primary-color)";
const NODE_WIDTH = 15;
const OVERFLOW_MARGIN = 5;
const FONT_SIZE = 12;
const MIN_DISTANCE = FONT_SIZE / 2;
const NODE_GAP = 8;
const LABEL_DISTANCE = 5;
@customElement("ha-sankey-chart")
export class HaSankeyChart extends LitElement {
@@ -65,141 +53,144 @@ export class HaSankeyChart extends LitElement {
@property({ type: Boolean }) public vertical = false;
@property({ attribute: false }) public loadingText?: string;
@property({ type: String, attribute: false }) public valueFormatter?: (
value: number
) => string;
private _statePerPixel = 0;
public chart?: EChartsType;
private _sizeController = new ResizeController(this, {
@state() private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect,
});
disconnectedCallback() {
super.disconnectedCallback();
}
willUpdate() {
this._statePerPixel = 0;
}
render() {
if (!this._sizeController.value) {
return this.loadingText ?? nothing;
}
const options = {
grid: {
top: 0,
bottom: 0,
left: 0,
right: 0,
},
tooltip: {
trigger: "item",
formatter: this._renderTooltip,
appendTo: document.body,
},
} as ECOption;
const { width, height } = this._sizeController.value;
const { nodes, paths } = this._processNodesAndPaths(
this.data.nodes,
this.data.links
);
return html`
<svg
width=${width}
height=${height}
viewBox="0 0 ${width} ${height}"
preserveAspectRatio="none"
>
<defs>
${paths.map(
(path, i) => svg`
<linearGradient id="gradient${path.sourceNode.id}.${path.targetNode.id}.${i}" gradientTransform="${
this.vertical ? "rotate(90)" : ""
}">
<stop offset="0%" stop-color="${path.sourceNode.color}"></stop>
<stop offset="100%" stop-color="${path.targetNode.color}"></stop>
</linearGradient>
`
)}
</defs>
${paths.map(
(path, i) =>
svg`
<path d="${path.path.map(([cmd, x, y]) => `${cmd}${x},${y}`).join(" ")} Z"
fill="url(#gradient${path.sourceNode.id}.${path.targetNode.id}.${i})" fill-opacity="0.4" />
`
)}
${nodes.map((node) =>
node.passThrough
? nothing
: svg`
<g transform="translate(${node.x},${node.y})">
<rect
class="node"
width=${this.vertical ? node.size : NODE_WIDTH}
height=${this.vertical ? NODE_WIDTH : node.size}
style="fill: ${node.color}"
>
<title>${node.tooltip}</title>
</rect>
${
this.vertical
? nothing
: svg`
<text
class="node-label"
x=${NODE_WIDTH + 5}
y=${node.size / 2}
text-anchor="start"
dominant-baseline="middle"
>${node.label}</text>
`
}
</g>
`
)}
</svg>
${this.vertical
? nodes.map((node) => {
if (!node.label) {
return nothing;
}
const labelWidth = MIN_DISTANCE + node.size;
const fontSize = this._getVerticalLabelFontSize(
node.label,
labelWidth
);
return html`<div
class="node-label vertical"
style="
left: ${node.x - MIN_DISTANCE / 2}px;
top: ${node.y + NODE_WIDTH}px;
width: ${labelWidth}px;
height: ${FONT_SIZE * 3}px;
font-size: ${fontSize}px;
line-height: ${fontSize}px;
"
title=${node.label}
>
${node.label}
</div>`;
})
: nothing}
`;
return html`<ha-chart-base
.data=${this._createData(this.data, this._sizeController.value?.width)}
.options=${options}
height="100%"
.extraComponents=${[SankeyChart]}
></ha-chart-base>`;
}
private _processNodesAndPaths = memoizeOne(
(rawNodes: Node[], rawLinks: Link[]) => {
const filteredNodes = rawNodes.filter((n) => n.value > 0);
const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();
const { links, passThroughNodes } = this._processLinks(
filteredNodes,
indexes,
rawLinks
);
const nodes = this._processNodes(
[...filteredNodes, ...passThroughNodes],
indexes
);
const paths = this._processPaths(nodes, links);
return { nodes, paths };
private _renderTooltip = (params: CallbackDataParams) => {
const data = params.data as Record<string, any>;
const value = this.valueFormatter
? this.valueFormatter(data.value)
: data.value;
if (data.id) {
const node = this.data.nodes.find((n) => n.id === data.id);
return `${params.marker} ${node?.label ?? data.id}<br>${value}`;
}
);
if (data.source && data.target) {
const source = this.data.nodes.find((n) => n.id === data.source);
const target = this.data.nodes.find((n) => n.id === data.target);
return `${source?.label ?? data.source}${target?.label ?? data.target}<br>${value}`;
}
return null;
};
private _processLinks(nodes: Node[], indexes: number[], rawLinks: Link[]) {
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
const filteredNodes = data.nodes.filter((n) => n.value > 0);
const indexes = [...new Set(filteredNodes.map((n) => n.index))];
const links = this._processLinks(filteredNodes, data.links);
const sectionWidth = width / indexes.length;
const labelSpace = sectionWidth - NODE_SIZE - LABEL_DISTANCE;
return {
id: "sankey",
type: "sankey",
nodes: filteredNodes.map((node) => ({
id: node.id,
value: node.value,
itemStyle: {
color: node.color,
},
depth: node.index,
})),
links,
draggable: false,
orient: this.vertical ? "vertical" : "horizontal",
nodeWidth: 15,
nodeGap: NODE_GAP,
lineStyle: {
color: "gradient",
opacity: 0.4,
},
layoutIterations: 0,
label: {
formatter: (params) =>
data.nodes.find((node) => node.id === (params.data as Node).id)
?.label ?? (params.data as Node).id,
position: this.vertical ? "bottom" : "right",
distance: LABEL_DISTANCE,
minMargin: 5,
overflow: "break",
},
labelLayout: (params) => {
if (this.vertical) {
// reduce the label font size so the longest word fits on one line
const longestWord = params.text
.split(" ")
.reduce(
(longest, current) =>
longest.length > current.length ? longest : current,
""
);
const wordWidth = measureTextWidth(longestWord, FONT_SIZE);
const fontSize = Math.min(
FONT_SIZE,
(params.rect.width / wordWidth) * FONT_SIZE
);
return {
fontSize: fontSize > 1 ? fontSize : 0,
width: params.rect.width,
align: "center",
};
}
// estimate the number of lines after the label is wrapped
// this is a very rough estimate, but it works for now
const lineCount = Math.ceil(params.labelRect.width / labelSpace);
// `overflow: "break"` allows the label to overflow outside its height, so we need to account for that
const fontSize = Math.min(
(params.rect.height / lineCount) * FONT_SIZE,
FONT_SIZE
);
return {
fontSize,
lineHeight: fontSize,
width: labelSpace,
height: params.rect.height,
};
},
top: this.vertical ? 0 : OVERFLOW_MARGIN,
bottom: this.vertical ? 25 : OVERFLOW_MARGIN,
left: this.vertical ? OVERFLOW_MARGIN : 0,
right: this.vertical ? OVERFLOW_MARGIN : labelSpace + LABEL_DISTANCE,
emphasis: {
focus: "adjacency",
},
} as SankeySeriesOption;
});
private _processLinks(nodes: Node[], rawLinks: Link[]) {
const accountedIn = new Map<string, number>();
const accountedOut = new Map<string, number>();
const links: ProcessedLink[] = [];
const passThroughNodes: Node[] = [];
rawLinks.forEach((link) => {
const sourceNode = nodes.find((n) => n.id === link.source);
const targetNode = nodes.find((n) => n.id === link.target);
@@ -222,307 +213,25 @@ export class HaSankeyChart extends LitElement {
accountedIn.set(targetNode.id, targetAccounted + value);
accountedOut.set(sourceNode.id, sourceAccounted + value);
// handle links across sections
const sourceIndex = indexes.findIndex((i) => i === sourceNode.index);
const targetIndex = indexes.findIndex((i) => i === targetNode.index);
const passThroughSections = indexes.slice(sourceIndex + 1, targetIndex);
// create pass-through nodes to reserve space
const passThroughNodeIds = passThroughSections.map((index) => {
const node = {
passThrough: true,
id: `${sourceNode.id}-${targetNode.id}-${index}`,
value,
index,
};
passThroughNodes.push(node);
return node.id;
});
if (value > 0) {
links.push({
...link,
value,
offset: {
source: sourceAccounted / (sourceNode.value || 1),
target: targetAccounted / (targetNode.value || 1),
},
passThroughNodeIds,
});
}
});
return { links, passThroughNodes };
}
private _processNodes(filteredNodes: Node[], indexes: number[]) {
// add MIN_DISTANCE as padding
const sectionSize = this.vertical
? this._sizeController.value!.width - MIN_DISTANCE * 2
: this._sizeController.value!.height - MIN_DISTANCE * 2;
const nodesPerSection: Record<number, Node[]> = {};
filteredNodes.forEach((node) => {
if (!nodesPerSection[node.index]) {
nodesPerSection[node.index] = [node];
} else {
nodesPerSection[node.index].push(node);
}
});
const sectionFlexSize = this._getSectionFlexSize(
Object.values(nodesPerSection)
);
const sections: Section[] = indexes.map((index, i) => {
const nodes: ProcessedNode[] = nodesPerSection[index].map(
(node: Node) => ({
...node,
color: node.color || DEFAULT_COLOR,
x: 0,
y: 0,
size: 0,
})
);
const availableSpace =
sectionSize - (nodes.length * MIN_DISTANCE - MIN_DISTANCE);
const totalValue = nodes.reduce(
(acc: number, node: Node) => acc + node.value,
0
);
const { nodes: sizedNodes, statePerPixel } = this._setNodeSizes(
nodes,
availableSpace,
totalValue
);
return {
nodes: sizedNodes,
offset: sectionFlexSize * i,
index,
totalValue,
statePerPixel,
};
});
sections.forEach((section) => {
// calc sizes again with the best statePerPixel
let totalSize = 0;
if (section.statePerPixel !== this._statePerPixel) {
section.nodes.forEach((node) => {
const size = Math.max(
MIN_SIZE,
Math.floor(node.value / this._statePerPixel)
);
totalSize += size;
node.size = size;
});
} else {
totalSize = section.nodes.reduce((sum, b) => sum + b.size, 0);
}
// calc margin between boxes
const emptySpace = sectionSize - totalSize;
const spacerSize = emptySpace / (section.nodes.length - 1);
// account for MIN_DISTANCE padding and center single node sections
let offset =
section.nodes.length > 1 ? MIN_DISTANCE : emptySpace / 2 + MIN_DISTANCE;
// calc positions - swap x/y for vertical layout
section.nodes.forEach((node) => {
if (this.vertical) {
node.x = offset;
node.y = section.offset;
} else {
node.x = section.offset;
node.y = offset;
}
offset += node.size + spacerSize;
});
});
return sections.flatMap((section) => section.nodes);
}
private _processPaths(nodes: ProcessedNode[], links: ProcessedLink[]) {
const flowDirection = this.vertical ? "y" : "x";
const orthDirection = this.vertical ? "x" : "y"; // orthogonal to the flow
const nodesById = new Map(nodes.map((n) => [n.id, n]));
return links.map((link) => {
const { source, target, value, offset, passThroughNodeIds } = link;
const pathNodes = [source, ...passThroughNodeIds, target].map(
(id) => nodesById.get(id)!
);
const offsets = [
offset.source,
...link.passThroughNodeIds.map(() => 0),
offset.target,
];
const sourceNode = pathNodes[0];
const targetNode = pathNodes[pathNodes.length - 1];
let path: [string, number, number][] = [
[
"M",
sourceNode[flowDirection] + NODE_WIDTH,
sourceNode[orthDirection] + offset.source * sourceNode.size,
],
]; // starting point
// traverse the path forwards. stop before the last node
for (let i = 0; i < pathNodes.length - 1; i++) {
const node = pathNodes[i];
const nextNode = pathNodes[i + 1];
const flowMiddle =
(nextNode[flowDirection] - node[flowDirection]) / 2 +
node[flowDirection];
const orthStart = node[orthDirection] + offsets[i] * node.size;
const orthEnd =
nextNode[orthDirection] + offsets[i + 1] * nextNode.size;
path.push(
["L", node[flowDirection] + NODE_WIDTH, orthStart],
["C", flowMiddle, orthStart],
["", flowMiddle, orthEnd],
["", nextNode[flowDirection], orthEnd]
);
}
// traverse the path backwards. stop before the first node
for (let i = pathNodes.length - 1; i > 0; i--) {
const node = pathNodes[i];
const prevNode = pathNodes[i - 1];
const flowMiddle =
(node[flowDirection] - prevNode[flowDirection]) / 2 +
prevNode[flowDirection];
const orthStart =
node[orthDirection] +
offsets[i] * node.size +
Math.max((value / (node.value || 1)) * node.size, 0);
const orthEnd =
prevNode[orthDirection] +
offsets[i - 1] * prevNode.size +
Math.max((value / (prevNode.value || 1)) * prevNode.size, 0);
path.push(
["L", node[flowDirection], orthStart],
["C", flowMiddle, orthStart],
["", flowMiddle, orthEnd],
["", prevNode[flowDirection] + NODE_WIDTH, orthEnd]
);
}
if (this.vertical) {
// Just swap x and y coordinates for vertical layout
path = path.map((c) => [c[0], c[2], c[1]]);
}
return {
sourceNode,
targetNode,
value,
path,
};
});
}
private _setNodeSizes(
nodes: ProcessedNode[],
availableSpace: number,
totalValue: number
): { nodes: ProcessedNode[]; statePerPixel: number } {
const statePerPixel = totalValue / availableSpace;
if (statePerPixel > this._statePerPixel) {
this._statePerPixel = statePerPixel;
}
let deficitHeight = 0;
const result = nodes.map((node) => {
if (node.size === MIN_SIZE) {
return node;
}
let size = Math.floor(node.value / this._statePerPixel);
if (size < MIN_SIZE) {
deficitHeight += MIN_SIZE - size;
size = MIN_SIZE;
}
return {
...node,
size,
};
});
if (deficitHeight > 0) {
return this._setNodeSizes(
result,
availableSpace - deficitHeight,
totalValue
);
}
return { nodes: result, statePerPixel: this._statePerPixel };
}
private _getSectionFlexSize(nodesPerSection: Node[][]): number {
const fullSize = this.vertical
? this._sizeController.value!.height
: this._sizeController.value!.width;
if (nodesPerSection.length < 2) {
return fullSize;
}
let lastSectionFlexSize: number;
if (this.vertical) {
lastSectionFlexSize = FONT_SIZE * 2 + NODE_WIDTH; // estimated based on the font size + some margin
} else {
// Estimate the width needed for the last section based on label length
const lastIndex = nodesPerSection.length - 1;
const lastSectionNodes = nodesPerSection[lastIndex];
const TEXT_PADDING = 5; // Padding between node and text
lastSectionFlexSize =
lastSectionNodes.length > 0
? Math.max(
...lastSectionNodes.map(
(node) =>
NODE_WIDTH +
TEXT_PADDING +
(node.label ? measureTextWidth(node.label, FONT_SIZE) : 0)
)
)
: 0;
}
// Calculate the flex size for other sections
const remainingSize = fullSize - lastSectionFlexSize;
const flexSize = remainingSize / (nodesPerSection.length - 1);
// if the last section is bigger than the others, we make them all the same size
// this is to prevent the last section from squishing the others
return lastSectionFlexSize < flexSize
? flexSize
: fullSize / nodesPerSection.length;
}
private _getVerticalLabelFontSize(label: string, labelWidth: number): number {
// reduce the label font size so the longest word fits on one line
const longestWord = label
.split(" ")
.reduce(
(longest, current) =>
longest.length > current.length ? longest : current,
""
);
const wordWidth = measureTextWidth(longestWord, FONT_SIZE);
return Math.min(FONT_SIZE, (labelWidth / wordWidth) * FONT_SIZE);
return links;
}
static styles = css`
:host {
display: block;
flex: 1;
background: var(--ha-card-background, var(--card-background-color, #000));
overflow: hidden;
position: relative;
background: var(--ha-card-background, var(--card-background-color));
}
svg {
overflow: visible;
position: absolute;
}
.node-label {
font-size: ${FONT_SIZE}px;
fill: var(--primary-text-color, white);
}
.node-label.vertical {
position: absolute;
text-align: center;
overflow: hidden;
ha-chart-base {
width: 100%;
height: 100%;
}
`;
}
@@ -63,6 +63,9 @@ export class StateHistoryChartLine extends LitElement {
@property({ type: String }) public height?: string;
@property({ attribute: "expand-legend", type: Boolean })
public expandLegend?: boolean;
@state() private _chartData: LineSeriesOption[] = [];
@state() private _entityIds: string[] = [];
@@ -87,9 +90,9 @@ export class StateHistoryChartLine extends LitElement {
.options=${this._chartOptions}
.height=${this.height}
style=${styleMap({ height: this.height })}
external-hidden
@dataset-hidden=${this._datasetHidden}
@dataset-unhidden=${this._datasetUnhidden}
.expandLegend=${this.expandLegend}
></ha-chart-base>
`;
}
@@ -256,7 +259,21 @@ export class StateHistoryChartLine extends LitElement {
axisLabel: {
margin: 5,
formatter: (value: number) => {
const label = formatNumber(value, this.hass.locale);
const formatOptions =
value >= 1 || value <= -1
? undefined
: {
// show the first significant digit for tiny values
maximumFractionDigits: Math.max(
2,
-Math.floor(Math.log10(Math.abs(value % 1 || 1)))
),
};
const label = formatNumber(
value,
this.hass.locale,
formatOptions
);
const width = measureTextWidth(label, 12) + 5;
if (width > this._yWidth) {
this._yWidth = width;
@@ -271,16 +288,12 @@ export class StateHistoryChartLine extends LitElement {
} as YAXisOption,
legend: {
show: this.showNames,
type: "scroll",
animationDurationUpdate: 400,
icon: "circle",
padding: [20, 0],
},
grid: {
...(this.showNames ? {} : { top: 30 }), // undefined is the same as 0
top: 15,
left: rtl ? 1 : Math.max(this.paddingYAxis, this._yWidth),
right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1,
bottom: 30,
bottom: 20,
},
visualMap: this._visualMap,
tooltip: {
@@ -354,9 +367,10 @@ export class StateHistoryChartLine extends LitElement {
name: nameY,
color,
symbol: "circle",
step: "end",
animationDurationUpdate: 0,
symbolSize: 1,
step: "end",
sampling: "minmax",
animationDurationUpdate: 0,
lineStyle: {
width: fill ? 0 : 1.5,
},
@@ -71,6 +71,9 @@ export class StateHistoryCharts extends LitElement {
@property({ type: String }) public height?: string;
@property({ attribute: "expand-legend", type: Boolean })
public expandLegend?: boolean;
private _computedStartTime!: Date;
private _computedEndTime!: Date;
@@ -154,6 +157,7 @@ export class StateHistoryCharts extends LitElement {
.fitYData=${this.fitYData}
@y-width-changed=${this._yWidthChanged}
.height=${this.virtualize ? undefined : this.height}
.expandLegend=${this.expandLegend}
></state-history-chart-line>
</div> `;
}
@@ -303,6 +307,8 @@ export class StateHistoryCharts extends LitElement {
.entry-container.line {
flex: 1;
padding-top: 8px;
overflow: hidden;
}
.entry-container:hover {
+31 -21
View File
@@ -1,6 +1,7 @@
import type {
BarSeriesOption,
LineSeriesOption,
ZRColor,
} from "echarts/types/dist/shared";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
@@ -90,6 +91,9 @@ export class StatisticsChart extends LitElement {
@property({ type: String }) public height?: string;
@property({ attribute: "expand-legend", type: Boolean })
public expandLegend?: boolean;
@state() private _chartData: (LineSeriesOption | BarSeriesOption)[] = [];
@state() private _legendData: string[] = [];
@@ -169,9 +173,9 @@ export class StatisticsChart extends LitElement {
.options=${this._chartOptions}
.height=${this.height}
style=${styleMap({ height: this.height })}
external-hidden
@dataset-hidden=${this._datasetHidden}
@dataset-unhidden=${this._datasetUnhidden}
.expandLegend=${this.expandLegend}
></ha-chart-base>
`;
}
@@ -301,14 +305,10 @@ export class StatisticsChart extends LitElement {
},
legend: {
show: !this.hideLegend,
type: "scroll",
animationDurationUpdate: 400,
icon: "circle",
padding: [20, 0],
data: this._legendData,
},
grid: {
...(this.hideLegend ? { top: this.unit ? 30 : 5 } : {}), // undefined is the same as 0
top: 15,
left: 1,
right: 1,
bottom: 0,
@@ -348,7 +348,11 @@ export class StatisticsChart extends LitElement {
let colorIndex = 0;
const statisticsData = Object.entries(this.statisticsData);
const totalDataSets: typeof this._chartData = [];
const legendData: { name: string; color: string }[] = [];
const legendData: {
name: string;
color?: ZRColor;
borderColor?: ZRColor;
}[] = [];
const statisticIds: string[] = [];
let endTime: Date;
@@ -399,7 +403,7 @@ export class StatisticsChart extends LitElement {
// The datasets for the current statistic
const statDataSets: (LineSeriesOption | BarSeriesOption)[] = [];
const statLegendData: { name: string; color: string }[] = [];
const statLegendData: typeof legendData = [];
const pushData = (
start: Date,
@@ -465,15 +469,6 @@ export class StatisticsChart extends LitElement {
sortedTypes.forEach((type) => {
if (statisticsHaveType(stats, type)) {
const band = drawBands && (type === "min" || type === "max");
if (!this.hideLegend) {
const showLegend = hasMean
? type === "mean"
: displayedLegend === false;
if (showLegend) {
statLegendData.push({ name, color });
}
displayedLegend = displayedLegend || showLegend;
}
statTypes.push(type);
const borderColor =
band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color;
@@ -492,8 +487,8 @@ export class StatisticsChart extends LitElement {
: this.hass.localize(
`ui.components.statistics_charts.statistic_types.${type}`
),
symbol: "circle",
symbolSize: 0,
symbol: "none",
sampling: "minmax",
animationDurationUpdate: 0,
lineStyle: {
width: 1.5,
@@ -511,13 +506,25 @@ export class StatisticsChart extends LitElement {
if (band && this.chartType === "line") {
series.stack = `band-${statistic_id}`;
series.stackStrategy = "all";
(series as LineSeriesOption).symbol = "none";
if (drawBands && type === "max") {
(series as LineSeriesOption).areaStyle = {
color: color + "3F",
};
}
}
if (!this.hideLegend) {
const showLegend = hasMean
? type === "mean"
: displayedLegend === false;
if (showLegend) {
statLegendData.push({
name,
color: series.color as ZRColor,
borderColor: series.itemStyle?.borderColor,
});
}
displayedLegend = displayedLegend || showLegend;
}
statDataSets.push(series);
statisticIds.push(statistic_id);
}
@@ -565,12 +572,15 @@ export class StatisticsChart extends LitElement {
this.unit = unit;
}
legendData.forEach(({ name, color }) => {
legendData.forEach(({ name, color, borderColor }) => {
// Add an empty series for the legend
totalDataSets.push({
id: name + "-legend",
name: name,
color,
itemStyle: {
borderColor,
},
type: this.chartType,
data: [],
xAxisIndex: 1,
@@ -1,7 +1,8 @@
import type { PropertyValues, TemplateResult } from "lit";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import "../ha-svg-icon";
import "../ha-tooltip";
@customElement("ha-data-table-icon")
class HaDataTableIcon extends LitElement {
@@ -9,30 +10,14 @@ class HaDataTableIcon extends LitElement {
@property() public path!: string;
@state() private _hovered = false;
protected render(): TemplateResult {
return html`
${this._hovered ? html`<div>${this.tooltip}</div>` : ""}
<ha-svg-icon .path=${this.path}></ha-svg-icon>
<ha-tooltip .content=${this.tooltip}>
<ha-svg-icon .path=${this.path}></ha-svg-icon>
</ha-tooltip>
`;
}
protected override firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
const show = () => {
this._hovered = true;
};
const hide = () => {
this._hovered = false;
};
this.addEventListener("mouseenter", show);
this.addEventListener("focus", show);
this.addEventListener("mouseleave", hide);
this.addEventListener("blur", hide);
this.addEventListener("tap", hide);
}
static styles = css`
:host {
display: inline-block;
@@ -41,20 +26,6 @@ class HaDataTableIcon extends LitElement {
ha-svg-icon {
color: var(--secondary-text-color);
}
div {
position: absolute;
right: 28px;
inset-inline-end: 28px;
inset-inline-start: initial;
z-index: 1002;
outline: none;
font-size: 10px;
line-height: 1;
background-color: var(--simple-tooltip-background, #616161);
color: var(--simple-tooltip-text-color, white);
padding: 8px;
border-radius: 2px;
}
`;
}
+9 -9
View File
@@ -1,4 +1,3 @@
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
@@ -6,6 +5,7 @@ import { computeStateName } from "../../common/entity/compute_state_name";
import type { HomeAssistant } from "../../types";
import "../ha-relative-time";
import "./state-badge";
import "../ha-tooltip";
@customElement("state-info")
class StateInfo extends LitElement {
@@ -36,13 +36,13 @@ class StateInfo extends LitElement {
</div>
${this.inDialog
? html`<div class="time-ago">
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
<simple-tooltip animation-delay="0">
<div>
<ha-tooltip>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
<div slot="content">
<div class="row">
<span class="column-name">
${this.hass.localize(
@@ -68,7 +68,7 @@ class StateInfo extends LitElement {
></ha-relative-time>
</div>
</div>
</simple-tooltip>
</ha-tooltip>
</div>`
: html`<div class="extra-info"><slot></slot></div>`}
</div>`;
+15 -16
View File
@@ -1,4 +1,3 @@
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
@@ -8,6 +7,7 @@ import type { Analytics, AnalyticsPreferences } from "../data/analytics";
import { haStyle } from "../resources/styles";
import "./ha-settings-row";
import "./ha-switch";
import "./ha-tooltip";
import type { HaSwitch } from "./ha-switch";
const ADDITIONAL_PREFERENCES = ["usage", "statistics"] as const;
@@ -67,22 +67,21 @@ export class HaAnalytics extends LitElement {
)}
</span>
<span>
<ha-switch
@change=${this._handleRowClick}
.checked=${this.analytics?.preferences[preference]}
.preference=${preference}
name=${preference}
<ha-tooltip
content=${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
)}
placement="right"
?disabled=${baseEnabled}
>
</ha-switch>
${!baseEnabled
? html`
<simple-tooltip animation-delay="0" position="right">
${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
)}
</simple-tooltip>
`
: ""}
<ha-switch
@change=${this._handleRowClick}
.checked=${this.analytics?.preferences[preference]}
.preference=${preference}
name=${preference}
>
</ha-switch>
</ha-tooltip>
</span>
</ha-settings-row>
`
+110
View File
@@ -0,0 +1,110 @@
import { customElement, property, state } from "lit/decorators";
import { css, html, LitElement, nothing } from "lit";
import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js";
import "./ha-button";
import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-textfield";
import type { HomeAssistant } from "../types";
import { copyToClipboard } from "../common/util/copy-clipboard";
import { showToast } from "../util/toast";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-copy-textfield")
export class HaCopyTextfield extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "value" }) public value!: string;
@property({ attribute: "masked-value" }) public maskedValue?: string;
@property({ attribute: "label" }) public label?: string;
@state() private _showMasked = true;
public render() {
return html`
<div class="container">
<div class="textfield-container">
<ha-textfield
.value=${this._showMasked && this.maskedValue
? this.maskedValue
: this.value}
readonly
.suffix=${this.maskedValue
? html`<div style="width: 24px"></div>`
: nothing}
@click=${this._focusInput}
></ha-textfield>
${this.maskedValue
? html`<ha-icon-button
class="toggle-unmasked"
.label=${this.hass.localize(
`ui.common.${this._showMasked ? "show" : "hide"}`
)}
@click=${this._toggleMasked}
.path=${this._showMasked ? mdiEye : mdiEyeOff}
></ha-icon-button>`
: nothing}
</div>
<ha-button @click=${this._copy} unelevated>
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
${this.label || this.hass.localize("ui.common.copy")}
</ha-button>
</div>
`;
}
private _focusInput(ev) {
const inputElement = ev.currentTarget as HaTextField;
inputElement.select();
}
private _toggleMasked(): void {
this._showMasked = !this._showMasked;
}
private async _copy(): Promise<void> {
await copyToClipboard(this.value);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static styles = css`
.container {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.textfield-container {
position: relative;
flex: 1;
}
.textfield-container ha-textfield {
display: block;
}
.toggle-unmasked {
position: absolute;
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-copy-textfield": HaCopyTextfield;
}
}
-1
View File
@@ -80,7 +80,6 @@ export class HaFormString extends LitElement implements HaFormElement {
if (!this.isPassword) return nothing;
return html`
<ha-icon-button
toggles
.label=${this.localize?.(
`${this.localizeBaseKey}.${
this.unmaskedPassword ? "hide_password" : "show_password"
+17 -9
View File
@@ -1,25 +1,33 @@
import { mdiHelpCircle } from "@mdi/js";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-svg-icon";
import "./ha-tooltip";
@customElement("ha-help-tooltip")
export class HaHelpTooltip extends LitElement {
@property() public label!: string;
@property() public position = "top";
@property() public position:
| "top"
| "bottom"
| "left"
| "right"
| "top-start"
| "top-end"
| "right-start"
| "right-end"
| "bottom-start"
| "bottom-end"
| "left-start"
| "left-end" = "top";
protected render(): TemplateResult {
return html`
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
<simple-tooltip
offset="4"
.position=${this.position}
.fitToVisibleBounds=${true}
>${this.label}</simple-tooltip
>
<ha-tooltip .placement=${this.position} .content=${this.label}>
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
</ha-tooltip>
`;
}
+8 -13
View File
@@ -1,7 +1,6 @@
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { mdiDotsVertical } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { haStyle } from "../resources/styles";
@@ -10,6 +9,7 @@ import "./ha-button-menu";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-svg-icon";
import "./ha-tooltip";
export interface IconOverflowMenuItem {
[key: string]: any;
@@ -70,25 +70,20 @@ export class HaIconOverflowMenu extends LitElement {
<!-- Icon representation for big screens -->
${this.items.map((item) =>
item.narrowOnly
? ""
? nothing
: item.divider
? html`<div role="separator"></div>`
: html`<div>
${item.tooltip
? html`<simple-tooltip
animation-delay="0"
position="left"
>
${item.tooltip}
</simple-tooltip>`
: ""}
: html`<ha-tooltip
.disabled=${!item.tooltip}
.content=${item.tooltip ?? ""}
>
<ha-icon-button
@click=${item.action}
.label=${item.label}
.path=${item.path}
?disabled=${item.disabled}
></ha-icon-button>
</div> `
</ha-tooltip>`
)}
`}
`;
-1
View File
@@ -1,5 +1,4 @@
import { mdiStar } from "@mdi/js";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
-1
View File
@@ -132,7 +132,6 @@ export class HaPasswordField extends LitElement {
@change=${this._handleChangeEvent}
></ha-textfield>
<ha-icon-button
toggles
.label=${this.hass?.localize(
this._unmaskedPassword
? "ui.components.selectors.text.hide_password"
+5
View File
@@ -250,6 +250,11 @@ export class HaPictureUpload extends LitElement {
max-height: 200px;
margin-bottom: 4px;
border-radius: var(--file-upload-image-border-radius);
transition: opacity 0.3s;
opacity: var(--picture-opacity, 1);
}
img:hover {
opacity: 1;
}
`,
];
+222
View File
@@ -0,0 +1,222 @@
import { customElement, property } from "lit/decorators";
import { css, html, LitElement, nothing } from "lit";
import "./ha-radio";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import type { HaRadio } from "./ha-radio";
import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types";
import { computeRTL } from "../common/util/compute_rtl";
interface SelectBoxOptionImage {
src: string;
src_dark?: string;
flip_rtl?: boolean;
}
export interface SelectBoxOption {
label?: string;
description?: string;
image?: string | SelectBoxOptionImage;
value: string;
disabled?: boolean;
}
@customElement("ha-select-box")
export class HaSelectBox extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public options: SelectBoxOption[] = [];
@property({ attribute: false }) public value?: string;
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Number, attribute: "max_columns" })
public maxColumns?: number;
render() {
const maxColumns = this.maxColumns ?? 3;
const columns = Math.min(maxColumns, this.options.length);
return html`
<div class="list" style=${styleMap({ "--columns": columns })}>
${this.options.map((option) => this._renderOption(option))}
</div>
`;
}
private _renderOption(option: SelectBoxOption) {
const horizontal = this.maxColumns === 1;
const disabled = option.disabled || this.disabled || false;
const selected = option.value === this.value;
const isDark = this.hass?.themes.darkMode || false;
const isRTL = this.hass ? computeRTL(this.hass) : false;
const imageSrc =
typeof option.image === "object"
? (isDark && option.image.src_dark) || option.image.src
: option.image;
const imageFlip =
typeof option.image === "object" ? isRTL && option.image.flip_rtl : false;
return html`
<label
class="option ${classMap({
horizontal: horizontal,
selected: selected,
})}"
?disabled=${disabled}
@click=${this._labelClick}
>
<div class="content">
<ha-radio
.checked=${option.value === this.value}
.value=${option.value}
.disabled=${disabled}
@change=${this._radioChanged}
></ha-radio>
<div class="text">
<span class="label">${option.label}</span>
${option.description
? html`<span class="description">${option.description}</span>`
: nothing}
</div>
</div>
${imageSrc
? html`
<img class=${imageFlip ? "flipped" : ""} alt="" src=${imageSrc} />
`
: nothing}
</label>
`;
}
private _labelClick(ev) {
ev.stopPropagation();
ev.currentTarget.querySelector("ha-radio")?.click();
}
private _radioChanged(ev: CustomEvent) {
const radio = ev.currentTarget as HaRadio;
const value = radio.value;
if (this.disabled || value === undefined || value === (this.value ?? "")) {
return;
}
fireEvent(this, "value-changed", {
value: value,
});
}
static styles = css`
.list {
display: grid;
grid-template-columns: repeat(var(--columns, 1), minmax(0, 1fr));
gap: 12px;
}
.option {
position: relative;
display: block;
border: 1px solid var(--divider-color);
border-radius: var(--ha-card-border-radius, 12px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 12px;
gap: 8px;
overflow: hidden;
cursor: pointer;
}
.option .content {
position: relative;
display: flex;
flex-direction: row;
gap: 8px;
min-width: 0;
width: 100%;
}
.option .content ha-radio {
margin: -12px;
flex: none;
}
.option .content .text {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
flex: 1;
}
.option .content .text .label {
color: var(--primary-text-color);
font-size: 14px;
font-weight: 400;
line-height: 20px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.option .content .text .description {
color: var(--secondary-text-color);
font-size: 13px;
font-weight: 400;
line-height: 16px;
}
img {
position: relative;
max-width: var(--ha-select-box-image-size, 96px);
max-height: var(--ha-select-box-image-size, 96px);
margin: auto;
}
.flipped {
transform: scaleX(-1);
}
.option.horizontal {
flex-direction: row;
align-items: flex-start;
}
.option.horizontal img {
margin: 0;
}
.option:before {
content: "";
display: block;
inset: 0;
position: absolute;
background-color: transparent;
pointer-events: none;
opacity: 0.2;
transition:
background-color 180ms ease-in-out,
opacity 180ms ease-in-out;
}
.option:hover:before {
background-color: var(--divider-color);
}
.option.selected:before {
background-color: var(--primary-color);
}
.option[disabled] {
cursor: not-allowed;
}
.option[disabled] .content,
.option[disabled] img {
opacity: 0.5;
}
.option[disabled]:before {
background-color: var(--disabled-color);
opacity: 0.05;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-select-box": HaSelectBox;
}
}
@@ -35,6 +35,14 @@ export class HaBackgroundSelector extends LitElement {
<div>
${this.yamlBackground
? html`
<div class="value">
<img
src=${this.value}
alt=${this.hass.localize(
"ui.components.picture-upload.current_image_alt"
)}
/>
</div>
<ha-alert alert-type="info">
${this.hass.localize(
`ui.components.selectors.background.yaml_info`
@@ -50,7 +58,7 @@ export class HaBackgroundSelector extends LitElement {
<ha-picture-upload
.hass=${this.hass}
.value=${this.value?.startsWith(URL_PREFIX) ? this.value : null}
.original=${this.selector.background?.original}
.original=${!!this.selector.background?.original}
.cropOptions=${this.selector.background?.crop}
select-media
@change=${this._pictureChanged}
@@ -83,6 +91,23 @@ export class HaBackgroundSelector extends LitElement {
white-space: nowrap;
--mdc-theme-primary: var(--primary-color);
}
.value {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
img {
max-width: 100%;
max-height: 200px;
margin-bottom: 4px;
border-radius: var(--file-upload-image-border-radius);
transition: opacity 0.3s;
opacity: var(--picture-opacity, 1);
}
img:hover {
opacity: 1;
}
`;
}
@@ -19,6 +19,7 @@ import "../ha-input-helper-text";
import "../ha-radio";
import "../ha-select";
import "../ha-sortable";
import "../ha-select-box";
@customElement("ha-selector-select")
export class HaSelectSelector extends LitElement {
@@ -91,6 +92,25 @@ export class HaSelectSelector extends LitElement {
);
}
if (
!this.selector.select?.multiple &&
!this.selector.select?.reorder &&
!this.selector.select?.custom_value &&
this._mode === "box"
) {
return html`
${this.label ? html`<span class="label">${this.label}</span>` : nothing}
<ha-select-box
.options=${options}
.value=${this.value as string | undefined}
@value-changed=${this._valueChanged}
.maxColumns=${this.selector.select?.box_max_columns}
.hass=${this.hass}
></ha-select-box>
${this._renderHelper()}
`;
}
if (
!this.selector.select?.custom_value &&
!this.selector.select?.reorder &&
@@ -269,7 +289,7 @@ export class HaSelectSelector extends LitElement {
: "";
}
private get _mode(): "list" | "dropdown" {
private get _mode(): "list" | "dropdown" | "box" {
return (
this.selector.select?.mode ||
((this.selector.select?.options?.length || 0) < 6 ? "list" : "dropdown")
@@ -411,6 +431,15 @@ export class HaSelectSelector extends LitElement {
padding: 8px 0;
}
.label {
display: block;
margin: 0 0 8px;
}
ha-select-box + ha-input-helper-text {
margin-top: 4px;
}
.sortable-fallback {
display: none;
opacity: 0;
@@ -95,7 +95,6 @@ export class HaTextSelector extends LitElement {
></ha-textfield>
${this.selector.text?.type === "password"
? html`<ha-icon-button
toggles
.label=${this.hass?.localize(
this._unmaskedPassword
? "ui.components.selectors.text.hide_password"
+17 -6
View File
@@ -20,6 +20,8 @@ export class HaTabs extends PaperTabs {
private _lastLeftHiddenState = false;
private _lastRightHiddenState = false;
static get template(): HTMLTemplateElement {
if (!subTemplate) {
subTemplate = (PaperTabs as any).template.cloneNode(true);
@@ -85,14 +87,23 @@ export class HaTabs extends PaperTabs {
this.$.tabsContainer.scrollLeft += dx;
const scrollLeft = this.$.tabsContainer.scrollLeft;
const dirRTL = this.dir === "rtl";
this._leftHidden = scrollLeft - this._firstTabWidth < 0;
this._rightHidden =
scrollLeft + this._lastTabWidth > this._tabContainerScrollSize;
const boolCondition1 = Math.abs(scrollLeft) < this._firstTabWidth;
const boolCondition2 =
Math.abs(scrollLeft) + this._lastTabWidth > this._tabContainerScrollSize;
if (this._lastLeftHiddenState !== this._leftHidden) {
this._lastLeftHiddenState = this._leftHidden;
this.$.tabsContainer.scrollLeft += this._leftHidden ? -23 : 23;
this._leftHidden = !dirRTL ? boolCondition1 : boolCondition2;
this._rightHidden = !dirRTL ? boolCondition2 : boolCondition1;
if (!dirRTL) {
if (this._lastLeftHiddenState !== this._leftHidden) {
this._lastLeftHiddenState = this._leftHidden;
this.$.tabsContainer.scrollLeft += this._leftHidden ? -23 : 23;
}
} else if (this._lastRightHiddenState !== this._rightHidden) {
this._lastRightHiddenState = this._rightHidden;
this.$.tabsContainer.scrollLeft -= this._rightHidden ? -23 : 23;
}
}
}
+32 -30
View File
@@ -1,4 +1,3 @@
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
// @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import "@material/mwc-button/mwc-button";
@@ -47,6 +46,7 @@ import { floorDefaultIconPath } from "./ha-floor-icon";
import "./ha-icon-button";
import "./ha-input-helper-text";
import "./ha-svg-icon";
import "./ha-tooltip";
@customElement("ha-target-picker")
export class HaTargetPicker extends SubscribeMixin(LitElement) {
@@ -339,38 +339,40 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
${type === "entity_id"
? ""
: html`<span role="gridcell">
<ha-icon-button
class="expand-btn mdc-chip__icon mdc-chip__icon--trailing"
.label=${this.hass.localize(
"ui.components.target-picker.expand"
)}
.path=${mdiUnfoldMoreVertical}
hide-title
.id=${id}
.type=${type}
@click=${this._handleExpand}
></ha-icon-button>
<simple-tooltip class="expand" animation-delay="0"
>${this.hass.localize(
<ha-tooltip
.content=${this.hass.localize(
`ui.components.target-picker.expand_${type}`
)}</simple-tooltip
)}
>
<ha-icon-button
class="expand-btn mdc-chip__icon mdc-chip__icon--trailing"
.label=${this.hass.localize(
"ui.components.target-picker.expand"
)}
.path=${mdiUnfoldMoreVertical}
hide-title
.id=${id}
.type=${type}
@click=${this._handleExpand}
></ha-icon-button>
</ha-tooltip>
</span>`}
<span role="gridcell">
<ha-icon-button
class="mdc-chip__icon mdc-chip__icon--trailing"
.label=${this.hass.localize("ui.components.target-picker.remove")}
.path=${mdiClose}
hide-title
.id=${id}
.type=${type}
@click=${this._handleRemove}
></ha-icon-button>
<simple-tooltip animation-delay="0"
>${this.hass.localize(
<ha-tooltip
.content=${this.hass.localize(
`ui.components.target-picker.remove_${type}`
)}</simple-tooltip
)}
>
<ha-icon-button
class="mdc-chip__icon mdc-chip__icon--trailing"
.label=${this.hass.localize("ui.components.target-picker.remove")}
.path=${mdiClose}
hide-title
.id=${id}
.type=${type}
@click=${this._handleRemove}
></ha-icon-button>
</ha-tooltip>
</span>
</div>
`;
@@ -828,9 +830,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.mdc-chip:hover {
z-index: 5;
}
simple-tooltip.expand {
min-width: 200px;
}
:host([disabled]) .mdc-chip {
opacity: var(--light-disabled-opacity);
pointer-events: none;
@@ -844,6 +843,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
display: block;
width: 100%;
}
ha-tooltip {
--ha-tooltip-arrow-size: 0;
}
`;
}
}
+41
View File
@@ -0,0 +1,41 @@
import SlTooltip from "@shoelace-style/shoelace/dist/components/tooltip/tooltip.component";
import styles from "@shoelace-style/shoelace/dist/components/tooltip/tooltip.styles";
import { css } from "lit";
import { customElement } from "lit/decorators";
import { setDefaultAnimation } from "@shoelace-style/shoelace/dist/utilities/animation-registry";
setDefaultAnimation("tooltip.show", {
keyframes: [{ opacity: 0 }, { opacity: 1 }],
options: { duration: 150, easing: "ease" },
});
setDefaultAnimation("tooltip.hide", {
keyframes: [{ opacity: 1 }, { opacity: 0 }],
options: { duration: 400, easing: "ease" },
});
@customElement("ha-tooltip")
export class HaTooltip extends SlTooltip {
static override styles = [
styles,
css`
:host {
--sl-tooltip-background-color: var(--secondary-background-color);
--sl-tooltip-color: var(--primary-text-color);
--sl-tooltip-font-family: Roboto, sans-serif;
--sl-tooltip-font-size: 12px;
--sl-tooltip-font-weight: normal;
--sl-tooltip-line-height: 1;
--sl-tooltip-padding: 8px;
--sl-tooltip-border-radius: var(--ha-tooltip-border-radius, 4px);
--sl-tooltip-arrow-size: var(--ha-tooltip-arrow-size, 8px);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-tooltip": HaTooltip;
}
}
+17
View File
@@ -581,6 +581,7 @@ export class HaMap extends ReactiveElement {
this._mapCluster = Leaflet.markerClusterGroup({
showCoverageOnHover: false,
removeOutsideVisibleBounds: false,
maxClusterRadius: 40,
});
this._mapCluster.addLayers(this._mapItems);
map.addLayer(this._mapCluster);
@@ -672,6 +673,22 @@ export class HaMap extends ReactiveElement {
box-shadow: none !important;
text-align: center;
}
.marker-cluster div {
background-clip: padding-box;
background-color: var(--primary-color);
border: 3px solid rgba(var(--rgb-primary-color), 0.2);
width: 32px;
height: 32px;
border-radius: 20px;
text-align: center;
color: var(--text-primary-color);
font-size: 14px;
}
.marker-cluster span {
line-height: 30px;
}
`;
}
@@ -4,7 +4,6 @@ import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import {
@@ -50,6 +49,7 @@ import "../ha-circular-progress";
import "../ha-fab";
import "../ha-icon-button";
import "../ha-svg-icon";
import "../ha-tooltip";
import "./ha-browse-media-tts";
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
import { loadVirtualizer } from "../../resources/virtualizer";
@@ -603,12 +603,9 @@ export class HaMediaPlayerBrowse extends LitElement {
`
: ""}
</div>
<div class="title">
${child.title}
<simple-tooltip fitToVisibleBounds position="top" offset="4"
>${child.title}</simple-tooltip
>
</div>
<ha-tooltip distance="-4" .content=${child.title}>
<div class="title">${child.title}</div>
</ha-tooltip>
</ha-card>
</div>
`;
+2
View File
@@ -38,6 +38,7 @@ export const sortWeekdays = (weekdays) =>
weekdays.sort((a, b) => BACKUP_DAYS.indexOf(a) - BACKUP_DAYS.indexOf(b));
export interface BackupConfig {
automatic_backups_configured: boolean;
last_attempted_automatic_backup: string | null;
last_completed_automatic_backup: string | null;
next_automatic_backup: string | null;
@@ -64,6 +65,7 @@ export interface BackupConfig {
}
export interface BackupMutableConfig {
automatic_backups_configured?: boolean;
create_backup?: {
agent_ids?: string[];
include_addons?: string[];
+1
View File
@@ -73,6 +73,7 @@ export interface CloudWebhook {
interface CloudLoginBase {
hass: HomeAssistant;
email: string;
check_connection?: boolean;
}
export interface CloudLoginPassword extends CloudLoginBase {
+8
View File
@@ -25,6 +25,12 @@ export interface LovelaceViewBackgroundConfig {
attachment?: "scroll" | "fixed";
}
export interface LovelaceViewHeaderConfig {
card?: LovelaceCardConfig;
layout?: "start" | "center" | "responsive";
badges_position?: "bottom" | "top";
}
export interface LovelaceBaseViewConfig {
index?: number;
title?: string;
@@ -39,6 +45,7 @@ export interface LovelaceBaseViewConfig {
// Only used for section view, it should move to a section view config type when the views will have dedicated editor.
max_columns?: number;
dense_section_placement?: boolean;
top_margin?: boolean;
}
export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
@@ -46,6 +53,7 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
badges?: (string | Partial<LovelaceBadgeConfig>)[]; // Badge can be just an entity_id or without type
cards?: LovelaceCardConfig[];
sections?: LovelaceSectionRawConfig[];
header?: LovelaceViewHeaderConfig;
}
export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig {
+1
View File
@@ -5,6 +5,7 @@ import type { HomeAssistant } from "../types";
export interface RecorderInfo {
backlog: number | null;
db_in_default_location: boolean;
max_backlog: number;
migration_in_progress: boolean;
migration_is_live: boolean;
+10 -1
View File
@@ -343,9 +343,17 @@ export interface AssistPipelineSelector {
} | null;
}
interface SelectBoxOptionImage {
src: string;
src_dark?: string;
flip_rtl?: boolean;
}
export interface SelectOption {
value: any;
label: string;
description?: string;
image?: string | SelectBoxOptionImage;
disabled?: boolean;
}
@@ -353,11 +361,12 @@ export interface SelectSelector {
select: {
multiple?: boolean;
custom_value?: boolean;
mode?: "list" | "dropdown";
mode?: "list" | "dropdown" | "box";
options: readonly string[] | readonly SelectOption[];
translation_key?: string;
sort?: boolean;
reorder?: boolean;
box_max_columns?: number;
} | null;
}
@@ -1,4 +1,3 @@
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import "@material/mwc-button";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
+7 -1
View File
@@ -40,8 +40,13 @@ export class DialogEnterCode
@state() private _showClearButton = false;
@state() private _narrow = false;
public async showDialog(dialogParams: EnterCodeDialogParams): Promise<void> {
this._dialogParams = dialogParams;
this._narrow = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
await this.updateComplete;
}
@@ -96,7 +101,7 @@ export class DialogEnterCode
>
<ha-textfield
class="input"
dialogInitialFocus
?dialogInitialFocus=${!this._narrow}
id="code"
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
type="password"
@@ -134,6 +139,7 @@ export class DialogEnterCode
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
type="password"
inputmode="numeric"
?dialogInitialFocus=${!this._narrow}
></ha-textfield>
<div class="keypad">
${BUTTONS.map((value) =>
@@ -99,7 +99,12 @@ class MoreInfoSirenAdvancedControls extends LitElement {
this._stateObj.attributes.available_tones
).map(
([toneId, toneName]) => html`
<ha-list-item .value=${toneId}
<ha-list-item
.value=${Array.isArray(
this._stateObj!.attributes.available_tones
)
? toneName
: toneId}
>${toneName}</ha-list-item
>
`
@@ -179,7 +184,7 @@ class MoreInfoSirenAdvancedControls extends LitElement {
await this.hass.callService("siren", "turn_on", {
entity_id: this._stateObj!.entity_id,
tone: this._tone,
volume: this._volume,
volume_level: this._volume,
duration: this._duration,
});
}
@@ -73,6 +73,7 @@ class MoreInfoUpdate extends LitElement {
if (updateType === "home_assistant") {
const isBackupConfigValid =
!!this._backupConfig &&
!!this._backupConfig.automatic_backups_configured &&
!!this._backupConfig.create_backup.password &&
this._backupConfig.create_backup.agent_ids.length > 0;
@@ -9,6 +9,7 @@ import { formatDateWeekdayShort } from "../../../common/datetime/format_date";
import { formatTime } from "../../../common/datetime/format_time";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import type {
ForecastEvent,
ModernForecastType,
@@ -157,14 +158,13 @@ class MoreInfoWeather extends LitElement {
${this.hass.formatEntityState(this.stateObj)}
</div>
<div class="time-ago">
<ha-relative-time
id="last_changed"
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
<simple-tooltip animation-delay="0" for="last_changed">
<div>
<ha-tooltip>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
<div slot="content">
<div class="row">
<span class="column-name">
${this.hass.localize(
@@ -190,7 +190,7 @@ class MoreInfoWeather extends LitElement {
></ha-relative-time>
</div>
</div>
</simple-tooltip>
</ha-tooltip>
</div>
</div>
<div class="temp-attribute">
@@ -1,10 +1,10 @@
import "@material/mwc-button";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { formatDateTime } from "../../common/datetime/format_date_time";
import "../../components/ha-markdown";
import "../../components/ha-relative-time";
import "../../components/ha-tooltip";
import "../../components/ha-button";
import type { PersistentNotification } from "../../data/persistent_notification";
import type { HomeAssistant } from "../../types";
import "./notification-item-template";
@@ -28,21 +28,23 @@ export class HuiPersistentNotificationItem extends LitElement {
<div class="time">
<span>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.notification.created_at}
capitalize
></ha-relative-time>
<simple-tooltip animation-delay="0">
${this._computeTooltip(this.hass, this.notification)}
</simple-tooltip>
<ha-tooltip
.content=${this._computeTooltip(this.hass, this.notification)}
placement="bottom"
>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.notification.created_at}
capitalize
></ha-relative-time>
</ha-tooltip>
</span>
</div>
<mwc-button slot="actions" @click=${this._handleDismiss}
<ha-button slot="actions" @click=${this._handleDismiss}
>${this.hass.localize(
"ui.card.persistent_notification.dismiss"
)}</mwc-button
)}</ha-button
>
</notification-item-template>
`;
+4
View File
@@ -251,6 +251,7 @@ export class QuickBar extends LitElement {
<mwc-list>
${this._opened
? html`<lit-virtualizer
tabindex="-1"
scroller
@keydown=${this._handleListItemKeyDown}
@rangechange=${this._handleRangeChanged}
@@ -326,6 +327,7 @@ export class QuickBar extends LitElement {
.twoline=${Boolean(item.area)}
.item=${item}
index=${ifDefined(index)}
tabindex="0"
>
<span>${item.primaryText}</span>
${item.area
@@ -346,6 +348,7 @@ export class QuickBar extends LitElement {
.item=${item}
index=${ifDefined(index)}
graphic="icon"
tabindex="0"
>
${item.iconPath
? html`
@@ -375,6 +378,7 @@ export class QuickBar extends LitElement {
index=${ifDefined(index)}
class="command-item"
hasMeta
tabindex="0"
>
<span>
<ha-label
@@ -10,6 +10,7 @@ import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import { cloudLogin } from "../../../data/cloud";
import { showCloudAlreadyConnectedDialog } from "../../../panels/config/cloud/dialog-cloud-already-connected/show-dialog-cloud-already-connected";
import type { HomeAssistant } from "../../../types";
import {
showAlertDialog,
@@ -25,6 +26,8 @@ export class CloudStepSignin extends LitElement {
@state() private _error?: string;
@state() private _checkConnection = true;
@query("#email", true) private _emailField!: HaTextField;
@query("#password", true) private _passwordField!: HaPasswordField;
@@ -115,6 +118,7 @@ export class CloudStepSignin extends LitElement {
hass: this.hass,
email: username,
...(code ? { code } : { password }),
check_connection: this._checkConnection,
});
} catch (err: any) {
const errCode = err && err.body && err.body.code;
@@ -139,6 +143,20 @@ export class CloudStepSignin extends LitElement {
}
}
if (errCode === "alreadyconnectederror") {
showCloudAlreadyConnectedDialog(this, {
details: JSON.parse(err.body.message),
logInHereAction: () => {
this._checkConnection = false;
doLogin(username);
},
closeDialog: () => {
this._requestInProgress = false;
},
});
return;
}
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
await doLogin(username.toLowerCase());
return;
@@ -1,5 +1,4 @@
import { ResizeController } from "@lit-labs/observers/resize-controller";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import "@material/mwc-button/mwc-button";
import {
mdiArrowDown,
+11 -2
View File
@@ -1,6 +1,7 @@
import type { PropertyValues } from "lit";
import { html } from "lit";
import { customElement, state } from "lit/decorators";
import type { Connection } from "home-assistant-js-websocket";
import { isNavigationClick } from "../common/dom/is-navigation-click";
import { navigate } from "../common/navigate";
import { getStorageDefaultPanelUrlPath } from "../data/panel";
@@ -22,6 +23,7 @@ import {
} from "../util/register-service-worker";
import "./ha-init-page";
import "./home-assistant-main";
import { storage } from "../common/decorators/storage";
const useHash = __DEMO__;
const curPath = () =>
@@ -40,6 +42,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
private _panelUrl: string;
@storage({ key: "ha-version", state: false, subscribe: false })
private _haVersion?: string;
private _hiddenTimeout?: number;
@@ -182,9 +185,15 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
protected hassReconnected() {
super.hassReconnected();
this._checkUpdate(this.hass!.connection);
}
private _checkUpdate(connection: Connection) {
const oldVersion = this._haVersion;
const currentVersion = connection.haVersion;
// If backend has been upgraded, make sure we update frontend
if (this.hass!.connection.haVersion !== this._haVersion) {
if (currentVersion !== oldVersion) {
this._haVersion = currentVersion;
if (supportsServiceWorker()) {
navigator.serviceWorker.getRegistration().then((registration) => {
if (registration) {
@@ -244,7 +253,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
}
const { auth, conn } = result;
this._haVersion = conn.haVersion;
this._checkUpdate(conn);
this.initializeHass(auth, conn);
} catch (_err: any) {
this._renderInitInfo(true);
@@ -1,5 +1,3 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
@@ -7,7 +5,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-aliases-editor";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import "../../../components/ha-settings-row";
@@ -17,7 +14,11 @@ import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
import "../../../components/ha-textfield";
import "../../../components/ha-labels-picker";
import type { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
import type {
AreaRegistryEntry,
AreaRegistryEntryMutableParams,
} from "../../../data/area_registry";
import { deleteAreaRegistryEntry } from "../../../data/area_registry";
import type { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@@ -26,6 +27,8 @@ import {
SENSOR_DEVICE_CLASS_HUMIDITY,
SENSOR_DEVICE_CLASS_TEMPERATURE,
} from "../../../data/sensor";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { createCloseHeading } from "../../../components/ha-dialog";
const cropOptions: CropOptions = {
round: false,
@@ -97,6 +100,130 @@ class DialogAreaDetail extends LitElement {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _renderSettings(entry: AreaRegistryEntry | undefined) {
return html`
${entry
? html`
<ha-settings-row>
<span slot="heading">
${this.hass.localize("ui.panel.config.areas.editor.area_id")}
</span>
<span slot="description"> ${entry.area_id} </span>
</ha-settings-row>
`
: nothing}
<ha-textfield
.value=${this._name}
@input=${this._nameChanged}
.label=${this.hass.localize("ui.panel.config.areas.editor.name")}
.validationMessage=${this.hass.localize(
"ui.panel.config.areas.editor.name_required"
)}
required
dialogInitialFocus
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
.value=${this._icon}
@value-changed=${this._iconChanged}
.label=${this.hass.localize("ui.panel.config.areas.editor.icon")}
></ha-icon-picker>
<ha-floor-picker
.hass=${this.hass}
.value=${this._floor}
@value-changed=${this._floorChanged}
.label=${this.hass.localize("ui.panel.config.areas.editor.floor")}
></ha-floor-picker>
<ha-labels-picker
.hass=${this.hass}
.value=${this._labels}
@value-changed=${this._labelsChanged}
></ha-labels-picker>
<ha-picture-upload
.hass=${this.hass}
.value=${this._picture}
crop
select-media
.cropOptions=${cropOptions}
@change=${this._pictureChanged}
></ha-picture-upload>
`;
}
private _renderAliasExpansion() {
return html`
<ha-expansion-panel
outlined
.header=${this.hass.localize(
"ui.panel.config.areas.editor.aliases_section"
)}
expanded
>
<div class="content">
<p class="description">
${this.hass.localize(
"ui.panel.config.areas.editor.aliases_description"
)}
</p>
<ha-aliases-editor
.hass=${this.hass}
.aliases=${this._aliases}
@value-changed=${this._aliasesChanged}
></ha-aliases-editor>
</div>
</ha-expansion-panel>
`;
}
private _renderRelatedEntitiesExpansion() {
return html`
<ha-expansion-panel
outlined
.header=${this.hass.localize(
"ui.panel.config.areas.editor.related_entities_section"
)}
expanded
>
<div class="content">
<ha-entity-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.areas.editor.temperature_entity"
)}
.helper=${this.hass.localize(
"ui.panel.config.areas.editor.temperature_entity_description"
)}
.value=${this._temperatureEntity}
.includeDomains=${SENSOR_DOMAINS}
.includeDeviceClasses=${TEMPERATURE_DEVICE_CLASSES}
.entityFilter=${this._areaEntityFilter}
@value-changed=${this._sensorChanged}
></ha-entity-picker>
<ha-entity-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.areas.editor.humidity_entity"
)}
.helper=${this.hass.localize(
"ui.panel.config.areas.editor.humidity_entity_description"
)}
.value=${this._humidityEntity}
.includeDomains=${SENSOR_DOMAINS}
.includeDeviceClasses=${HUMIDITY_DEVICE_CLASSES}
.entityFilter=${this._areaEntityFilter}
@value-changed=${this._sensorChanged}
></ha-entity-picker>
</div>
</ha-expansion-panel>
`;
}
protected render() {
if (!this._params) {
return nothing;
@@ -104,6 +231,7 @@ class DialogAreaDetail extends LitElement {
const entry = this._params.entry;
const nameInvalid = !this._isNameValid();
const isNew = !entry;
return html`
<ha-dialog
open
@@ -120,123 +248,32 @@ class DialogAreaDetail extends LitElement {
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
${entry
? html`
<ha-settings-row>
<span slot="heading">
${this.hass.localize(
"ui.panel.config.areas.editor.area_id"
)}
</span>
<span slot="description"> ${entry.area_id} </span>
</ha-settings-row>
`
: nothing}
<ha-textfield
.value=${this._name}
@input=${this._nameChanged}
.label=${this.hass.localize("ui.panel.config.areas.editor.name")}
.validationMessage=${this.hass.localize(
"ui.panel.config.areas.editor.name_required"
)}
required
dialogInitialFocus
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
.value=${this._icon}
@value-changed=${this._iconChanged}
.label=${this.hass.localize("ui.panel.config.areas.editor.icon")}
></ha-icon-picker>
<ha-floor-picker
.hass=${this.hass}
.value=${this._floor}
@value-changed=${this._floorChanged}
.label=${this.hass.localize("ui.panel.config.areas.editor.floor")}
></ha-floor-picker>
<ha-labels-picker
.hass=${this.hass}
.value=${this._labels}
@value-changed=${this._labelsChanged}
></ha-labels-picker>
<ha-picture-upload
.hass=${this.hass}
.value=${this._picture}
crop
select-media
.cropOptions=${cropOptions}
@change=${this._pictureChanged}
></ha-picture-upload>
<h3 class="header">
${this.hass.localize(
"ui.panel.config.areas.editor.aliases_section"
)}
</h3>
<p class="description">
${this.hass.localize(
"ui.panel.config.areas.editor.aliases_description"
)}
</p>
<ha-aliases-editor
.hass=${this.hass}
.aliases=${this._aliases}
@value-changed=${this._aliasesChanged}
></ha-aliases-editor>
${!isNew
? html`
<ha-entity-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.areas.editor.temperature_entity"
)}
.helper=${this.hass.localize(
"ui.panel.config.areas.editor.temperature_entity_description"
)}
.value=${this._temperatureEntity}
.includeDomains=${SENSOR_DOMAINS}
.includeDeviceClasses=${TEMPERATURE_DEVICE_CLASSES}
.entityFilter=${this._areaEntityFilter}
@value-changed=${this._sensorChanged}
></ha-entity-picker>
<ha-entity-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.areas.editor.humidity_entity"
)}
.helper=${this.hass.localize(
"ui.panel.config.areas.editor.humidity_entity_description"
)}
.value=${this._humidityEntity}
.includeDomains=${SENSOR_DOMAINS}
.includeDeviceClasses=${HUMIDITY_DEVICE_CLASSES}
.entityFilter=${this._areaEntityFilter}
@value-changed=${this._sensorChanged}
></ha-entity-picker>
`
: ""}
${this._renderSettings(entry)} ${this._renderAliasExpansion()}
${!isNew ? this._renderRelatedEntitiesExpansion() : nothing}
</div>
</div>
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${nameInvalid || this._submitting}
>
${entry
? this.hass.localize("ui.common.save")
: this.hass.localize("ui.common.create")}
</mwc-button>
${!isNew
? html`<ha-button
slot="secondaryAction"
destructive
@click=${this._deleteArea}
>
${this.hass.localize("ui.common.delete")}
</ha-button>`
: nothing}
<div slot="primaryAction">
<ha-button @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
@click=${this._updateEntry}
.disabled=${nameInvalid || this._submitting}
>
${entry
? this.hass.localize("ui.common.save")
: this.hass.localize("ui.common.create")}
</ha-button>
</div>
</ha-dialog>
`;
}
@@ -325,6 +362,31 @@ class DialogAreaDetail extends LitElement {
}
}
private async _deleteArea() {
if (!this._params?.entry) {
return;
}
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.areas.delete.confirmation_title",
{ name: this._params.entry.name }
),
text: this.hass.localize(
"ui.panel.config.areas.delete.confirmation_text"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
});
if (!confirmed) {
return;
}
await deleteAreaRegistryEntry(this.hass!, this._params!.entry!.area_id);
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
@@ -332,15 +394,28 @@ class DialogAreaDetail extends LitElement {
ha-textfield {
display: block;
}
ha-expansion-panel {
--expansion-panel-content-padding: 0;
}
ha-aliases-editor,
ha-entity-picker,
ha-floor-picker,
ha-icon-picker,
ha-labels-picker,
ha-picture-upload {
ha-picture-upload,
ha-expansion-panel {
display: block;
margin-bottom: 16px;
}
ha-dialog {
--mdc-dialog-min-width: min(600px, 100vw);
}
.content {
padding: 12px;
}
.description {
margin: 0 0 16px 0;
}
`,
];
}
+13 -18
View File
@@ -20,6 +20,7 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-list-item";
import "../../../components/ha-button-menu";
import "../../../components/ha-tooltip";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import {
deleteAreaRegistryEntry,
@@ -549,7 +550,11 @@ class HaConfigAreaPage extends LitElement {
}
private _renderScene(name: string, entityState: SceneEntity) {
return html`<div>
return html`<ha-tooltip
.distance=${-4}
.disabled=${!!entityState.attributes.id}
.content=${this.hass.localize("ui.panel.config.devices.cant_edit")}
>
<a
href=${ifDefined(
entityState.attributes.id
@@ -562,18 +567,15 @@ class HaConfigAreaPage extends LitElement {
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
${!entityState.attributes.id
? html`
<simple-tooltip animation-delay="0">
${this.hass.localize("ui.panel.config.devices.cant_edit")}
</simple-tooltip>
`
: ""}
</div>`;
</ha-tooltip>`;
}
private _renderAutomation(name: string, entityState: AutomationEntity) {
return html`<div>
return html`<ha-tooltip
.disabled=${!!entityState.attributes.id}
.distance=${-4}
.content=${this.hass.localize("ui.panel.config.devices.cant_edit")}
>
<a
href=${ifDefined(
entityState.attributes.id
@@ -586,14 +588,7 @@ class HaConfigAreaPage extends LitElement {
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
${!entityState.attributes.id
? html`
<simple-tooltip animation-delay="0">
${this.hass.localize("ui.panel.config.devices.cant_edit")}
</simple-tooltip>
`
: ""}
</div>`;
</ha-tooltip>`;
}
private _renderScript(name: string, entityState: ScriptEntity) {
@@ -33,6 +33,7 @@ import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-service-icon";
import "../../../../components/ha-tooltip";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import type { AutomationClipboard } from "../../../../data/automation";
@@ -231,14 +232,14 @@ export default class HaAutomationActionRow extends LitElement {
${type !== "condition" &&
(this.action as NonConditionAction).continue_on_error === true
? html`<div slot="icons">
? html`<ha-tooltip
slot="icons"
.content=${this.hass.localize(
"ui.panel.config.automation.editor.actions.continue_on_error"
)}
>
<ha-svg-icon .path=${mdiAlertCircleCheck}></ha-svg-icon>
<simple-tooltip animation-delay="0">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.continue_on_error"
)}
</simple-tooltip>
</div> `
</ha-tooltip>`
: nothing}
<ha-md-button-menu
@@ -659,6 +660,9 @@ export default class HaAutomationActionRow extends LitElement {
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
ha-tooltip {
cursor: default;
}
`,
];
}
@@ -1,6 +1,5 @@
import { consume } from "@lit-labs/context";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import {
mdiChevronRight,
mdiCog,
@@ -58,7 +57,6 @@ import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-menu-item";
import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
import "../../../components/ha-menu";
@@ -24,6 +24,7 @@ import { fetchHassioAddonsInfo } from "../../../../../data/hassio/addon";
import type { HomeAssistant } from "../../../../../types";
import "../ha-backup-addons-picker";
import type { BackupAddonItem } from "../ha-backup-addons-picker";
import { getRecorderInfo } from "../../../../../data/recorder";
export interface FormData {
homeassistant: boolean;
@@ -75,8 +76,11 @@ class HaBackupConfigData extends LitElement {
@state() private _showAddons = false;
@state() private _showDbOption = true;
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this._checkDbOption();
if (isComponentLoaded(this.hass, "hassio")) {
this._fetchAddons();
}
@@ -98,6 +102,18 @@ class HaBackupConfigData extends LitElement {
fireEvent(this, "backup-addons-fetched");
}
private async _checkDbOption() {
if (isComponentLoaded(this.hass, "recorder")) {
const info = await getRecorderInfo(this.hass.connection);
this._showDbOption = info.db_in_default_location;
if (!this._showDbOption && this.value?.include_database) {
this.value.include_database = false;
}
} else {
this._showDbOption = false;
}
}
private _hasLocalAddons(addons: BackupAddonItem[]): boolean {
return addons.some((addon) => addon.slug === "local");
}
@@ -179,24 +195,25 @@ class HaBackupConfigData extends LitElement {
></ha-switch>
</ha-md-list-item>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiChartBox}></ha-svg-icon>
<span slot="headline">
${this.hass.localize("ui.panel.config.backup.data.history")}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.data.history_description"
)}
</span>
<ha-switch
id="database"
slot="end"
@change=${this._switchChanged}
.checked=${data.database}
></ha-switch>
</ha-md-list-item>
${this._showDbOption
? html`<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiChartBox}></ha-svg-icon>
<span slot="headline">
${this.hass.localize("ui.panel.config.backup.data.history")}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.data.history_description"
)}
</span>
<ha-switch
id="database"
slot="end"
@change=${this._switchChanged}
.checked=${data.database}
></ha-switch>
</ha-md-list-item>`
: nothing}
${isHassio
? html`
<ha-md-list-item>
@@ -1,7 +1,7 @@
import { mdiCalendar, mdiDatabase, mdiPuzzle, mdiUpload } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
@@ -18,6 +18,8 @@ import {
} from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { getRecorderInfo } from "../../../../../data/recorder";
@customElement("ha-backup-overview-settings")
class HaBackupBackupsSummary extends LitElement {
@@ -27,10 +29,26 @@ class HaBackupBackupsSummary extends LitElement {
@property({ attribute: false }) public agents!: BackupAgent[];
@state() private _showDbOption = true;
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this._checkDbOption();
}
private _configure() {
navigate("/config/backup/settings");
}
private async _checkDbOption() {
if (isComponentLoaded(this.hass, "recorder")) {
const info = await getRecorderInfo(this.hass.connection);
this._showDbOption = info.db_in_default_location;
} else {
this._showDbOption = false;
}
}
private _scheduleDescription(config: BackupConfig): string {
const { copies, days } = config.retention;
const { recurrence } = config.schedule;
@@ -214,7 +232,8 @@ class HaBackupBackupsSummary extends LitElement {
<ha-md-list-item type="link" href="/config/backup/settings#data">
<ha-svg-icon slot="start" .path=${mdiDatabase}></ha-svg-icon>
<div slot="headline">
${this.config.create_backup.include_database
${this._showDbOption &&
this.config.create_backup.include_database
? this.hass.localize(
"ui.panel.config.backup.overview.settings.data_settings_history"
)
@@ -1,5 +1,5 @@
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
@@ -53,7 +53,10 @@ type Step = (typeof STEPS)[number];
const FULL_DIALOG_STEPS = new Set<Step>(["setup"]);
const SAVE_STEPS = new Set<Step>(["schedule", "data", "locations"]);
const RECOMMENDED_CONFIG: BackupConfig = {
automatic_backups_configured: false,
create_backup: {
agent_ids: [],
include_folders: [],
@@ -95,29 +98,23 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
public showDialog(params: BackupOnboardingDialogParams): void {
this._params = params;
this._step = this._firstStep;
this._config = RECOMMENDED_CONFIG;
const agents: string[] = [];
// Enable local location by default
if (isComponentLoaded(this.hass, "hassio")) {
agents.push(HASSIO_LOCAL_AGENT);
if (this._params.config?.create_backup.password) {
// onboarding wizard was started before, but not finished.
this._config = this._params.config;
this._step = "setup";
} else {
agents.push(CORE_LOCAL_AGENT);
}
// Enable cloud location if logged in
if (this._params.cloudStatus?.logged_in) {
agents.push(CLOUD_AGENT);
this._step = this._firstStep;
this._config = {
...RECOMMENDED_CONFIG,
create_backup: {
...RECOMMENDED_CONFIG.create_backup,
agent_ids: this._defaultAgents,
password: generateEncryptionKey(),
},
};
}
this._config = {
...this._config,
create_backup: {
...this._config.create_backup,
agent_ids: agents,
password: generateEncryptionKey(),
},
};
this._opened = true;
}
@@ -139,7 +136,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
return this._params?.skipWelcome ? STEPS[1] : STEPS[0];
}
private async _done() {
private async _save(done = false) {
if (!this._config) {
return;
}
@@ -152,6 +149,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
},
schedule: this._config.schedule,
retention: this._config.retention,
automatic_backups_configured: done,
};
if (isComponentLoaded(this.hass, "hassio")) {
@@ -163,9 +161,12 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
this._config.create_backup.include_addons || [];
}
try {
await updateBackupConfig(this.hass, params);
await updateBackupConfig(this.hass, params);
}
private async _done() {
try {
await this._save(true);
this._params?.submit!(true);
this._dialog.close();
} catch (err) {
@@ -184,6 +185,9 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
}
private _nextStep() {
if (this._step && SAVE_STEPS.has(this._step)) {
this._save();
}
const index = STEPS.indexOf(this._step!);
if (index === STEPS.length - 1) {
return;
@@ -191,6 +195,12 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
this._step = STEPS[index + 1];
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_step") && this._step === "key") {
this._save();
}
}
protected render() {
if (!this._opened || !this._params || !this._step) {
return nothing;
@@ -250,6 +260,36 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
`;
}
private get _defaultAgents(): string[] {
const agents: string[] = [];
// Enable local location by default
if (isComponentLoaded(this.hass, "hassio")) {
agents.push(HASSIO_LOCAL_AGENT);
} else {
agents.push(CORE_LOCAL_AGENT);
}
// Enable cloud location if logged in
if (this._params?.cloudStatus?.logged_in) {
agents.push(CLOUD_AGENT);
}
return agents;
}
private _useRecommended() {
if (!this._config?.create_backup.password) {
return;
}
this._config = {
...RECOMMENDED_CONFIG,
create_backup: {
...RECOMMENDED_CONFIG.create_backup,
agent_ids: this._defaultAgents,
password: this._config.create_backup.password,
},
};
this._done();
}
private get _stepTitle(): string {
switch (this._step) {
case "key":
@@ -345,7 +385,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
case "setup":
return html`
<ha-md-list class="full">
<ha-md-list-item type="button" @click=${this._done}>
<ha-md-list-item type="button" @click=${this._useRecommended}>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.dialogs.onboarding.setup.recommended_heading"
@@ -356,7 +396,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
"ui.panel.config.backup.dialogs.onboarding.setup.recommended_description"
)}
</span>
<ha-icon-next slot="end"> </ha-icon-next>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item type="button" @click=${this._nextStep}>
<span slot="headline">
@@ -369,7 +409,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
"ui.panel.config.backup.dialogs.onboarding.setup.custom_description"
)}
</span>
<ha-icon-next slot="end"> </ha-icon-next>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
`;
@@ -1,9 +1,11 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import type { BackupConfig } from "../../../../data/backup";
import type { CloudStatus } from "../../../../data/cloud";
export interface BackupOnboardingDialogParams {
submit?: (value: boolean) => void;
cancel?: () => void;
config: BackupConfig;
cloudStatus?: CloudStatus;
skipWelcome?: boolean;
}
@@ -428,15 +428,9 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
"ui.panel.config.backup.backups.delete_selected"
)}
.path=${mdiDelete}
id="delete-btn"
class="warning"
@click=${this._deleteSelected}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="delete-btn">
${this.hass.localize(
"ui.panel.config.backup.backups.delete_selected"
)}
</simple-tooltip>
`}
</div>
@@ -505,7 +499,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
}
private get _needsOnboarding() {
return !this.config?.create_backup.password;
return !this.config?.automatic_backups_configured;
}
private async _uploadBackup(ev) {
@@ -78,6 +78,7 @@ class HaConfigBackupOverview extends LitElement {
private async _setupAutomaticBackup(skipWelcome = false) {
const success = await showBackupOnboardingDialog(this, {
config: this.config!,
cloudStatus: this.cloudStatus,
skipWelcome,
});
@@ -128,7 +129,7 @@ class HaConfigBackupOverview extends LitElement {
}
private get _needsOnboarding() {
return this.config && !this.config.create_backup.password;
return this.config && !this.config.automatic_backups_configured;
}
protected render(): TemplateResult {
@@ -1,4 +1,3 @@
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import {
mdiAlertCircle,
mdiDelete,
@@ -1,17 +1,13 @@
import { mdiContentCopy, mdiEye, mdiEyeOff, mdiHelpCircle } from "@mdi/js";
import { mdiHelpCircle } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-formfield";
import "../../../../components/ha-radio";
import "../../../../components/ha-settings-row";
import "../../../../components/ha-switch";
import "../../../../components/ha-textfield";
import { formatDate } from "../../../../common/datetime/format_date";
import type { HaSwitch } from "../../../../components/ha-switch";
@@ -25,6 +21,7 @@ import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
import { obfuscateUrl } from "../../../../util/url";
import "../../../../components/ha-copy-textfield";
@customElement("cloud-remote-pref")
export class CloudRemotePref extends LitElement {
@@ -34,8 +31,6 @@ export class CloudRemotePref extends LitElement {
@property({ type: Boolean }) public narrow = false;
@state() private _unmaskedUrl = false;
protected render() {
if (!this.cloudStatus) {
return nothing;
@@ -139,37 +134,13 @@ export class CloudRemotePref extends LitElement {
)}
</p>
`}
<div class="url-container">
<div class="textfield-container">
<ha-textfield
.value=${this._unmaskedUrl
? `https://${remote_domain}`
: obfuscateUrl(`https://${remote_domain}`)}
readonly
.suffix=${
// reserve some space for the icon.
html`<div style="width: 24px"></div>`
}
></ha-textfield>
<ha-icon-button
class="toggle-unmasked-url"
toggles
.label=${this.hass.localize(
`ui.panel.config.common.${this._unmaskedUrl ? "hide" : "show"}_url`
)}
@click=${this._toggleUnmaskedUrl}
.path=${this._unmaskedUrl ? mdiEyeOff : mdiEye}
></ha-icon-button>
</div>
<ha-button
.url=${`https://${remote_domain}`}
@click=${this._copyURL}
unelevated
>
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
${this.hass.localize("ui.panel.config.common.copy_link")}
</ha-button>
</div>
<ha-copy-textfield
.hass=${this.hass}
.value=${`https://${remote_domain}`}
.maskedValue=${obfuscateUrl(`https://${remote_domain}`)}
.label=${this.hass!.localize("ui.panel.config.common.copy_link")}
></ha-copy-textfield>
<ha-expansion-panel
outlined
@@ -234,10 +205,6 @@ export class CloudRemotePref extends LitElement {
});
}
private _toggleUnmaskedUrl(): void {
this._unmaskedUrl = !this._unmaskedUrl;
}
private async _toggleChanged(ev) {
const toggle = ev.target as HaSwitch;
@@ -268,14 +235,6 @@ export class CloudRemotePref extends LitElement {
}
}
private async _copyURL(ev): Promise<void> {
const url = ev.currentTarget.url;
await copyToClipboard(url);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static styles = css`
.preparing {
padding: 0 16px 16px;
@@ -335,30 +294,6 @@ export class CloudRemotePref extends LitElement {
display: block;
margin-bottom: 16px;
}
.url-container {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.textfield-container {
position: relative;
flex: 1;
}
.textfield-container ha-textfield {
display: block;
}
.toggle-unmasked-url {
position: absolute;
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
hr {
border: none;
height: 1px;
@@ -0,0 +1,171 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { mdiEye, mdiEyeOff } from "@mdi/js";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
import { createCloseHeading } from "../../../../components/ha-dialog";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { CloudAlreadyConnectedParams as CloudAlreadyConnectedDialogParams } from "./show-dialog-cloud-already-connected";
import { obfuscateUrl } from "../../../../util/url";
@customElement("dialog-cloud-already-connected")
class DialogCloudAlreadyConnected extends LitElement {
public hass!: HomeAssistant;
@state() private _params?: CloudAlreadyConnectedDialogParams;
@state() private _obfuscateIp = true;
public showDialog(params: CloudAlreadyConnectedDialogParams) {
this._params = params;
}
public closeDialog() {
this._params?.closeDialog();
this._params = undefined;
this._obfuscateIp = true;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
const { details } = this._params;
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.heading"
)
)}
>
<div class="intro">
<span>
${this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.description"
)}
</span>
<b>
${this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.other_home_assistant"
)}
</b>
</div>
<div class="instance-details">
<div class="instance-detail">
<span>
${this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.ip_address"
)}:
</span>
<div class="obfuscated">
<span>
${this._obfuscateIp
? obfuscateUrl(details.remote_ip_address)
: details.remote_ip_address}
</span>
<ha-icon-button
class="toggle-unmasked-url"
.label=${this.hass.localize(
`ui.panel.config.cloud.dialog_already_connected.obfuscated_ip.${this._obfuscateIp ? "hide" : "show"}`
)}
@click=${this._toggleObfuscateIp}
.path=${this._obfuscateIp ? mdiEye : mdiEyeOff}
></ha-icon-button>
</div>
</div>
<div class="instance-detail">
<span>
${this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.connected_at"
)}:
</span>
<span>
${formatDateTime(
new Date(details.connected_at),
this.hass.locale,
this.hass.config
)}
</span>
</div>
</div>
<ha-alert
alert-type="info"
.title=${this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.info_backups.title"
)}
>
${this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.info_backups.description"
)}
</ha-alert>
<ha-button @click=${this.closeDialog} slot="secondaryAction">
${this.hass!.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._logInHere} slot="primaryAction">
${this.hass!.localize(
"ui.panel.config.cloud.dialog_already_connected.login_here"
)}
</ha-button>
</ha-dialog>
`;
}
private _toggleObfuscateIp() {
this._obfuscateIp = !this._obfuscateIp;
}
private _logInHere() {
this._params?.logInHereAction();
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 535px;
}
.intro b {
display: block;
margin-top: 16px;
}
.instance-details {
display: flex;
flex-direction: column;
margin-bottom: 16px;
}
.instance-detail {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.obfuscated {
align-items: center;
display: flex;
flex-direction: row;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-cloud-already-connected": DialogCloudAlreadyConnected;
}
}
@@ -0,0 +1,21 @@
import { fireEvent } from "../../../../common/dom/fire_event";
export interface CloudAlreadyConnectedParams {
details: {
remote_ip_address: string;
connected_at: string;
};
logInHereAction: () => void;
closeDialog: () => void;
}
export const showCloudAlreadyConnectedDialog = (
element: HTMLElement,
webhookDialogParams: CloudAlreadyConnectedParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-cloud-already-connected",
dialogImport: () => import("./dialog-cloud-already-connected"),
dialogParams: webhookDialogParams,
});
};
@@ -1,27 +1,23 @@
import "@material/mwc-button";
import { mdiContentCopy, mdiOpenInNew } from "@mdi/js";
import { mdiOpenInNew } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { query, state } from "lit/decorators";
import { state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { documentationUrl } from "../../../../util/documentation-url";
import { showToast } from "../../../../util/toast";
import type { WebhookDialogParams } from "./show-dialog-manage-cloudhook";
import "../../../../components/ha-copy-textfield";
export class DialogManageCloudhook extends LitElement {
protected hass?: HomeAssistant;
@state() private _params?: WebhookDialogParams;
@query("ha-textfield") _input!: HaTextField;
public showDialog(params: WebhookDialogParams) {
this._params = params;
}
@@ -82,21 +78,12 @@ export class DialogManageCloudhook extends LitElement {
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
</p>
<ha-textfield
.label=${this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.public_url"
)}
<ha-copy-textfield
.hass=${this.hass}
.value=${cloudhook.cloudhook_url}
iconTrailing
readOnly
@click=${this._focusInput}
>
<ha-icon-button
@click=${this._copyUrl}
slot="trailingIcon"
.path=${mdiContentCopy}
></ha-icon-button>
</ha-textfield>
.label=${this.hass!.localize("ui.panel.config.common.copy_link")}
></ha-copy-textfield>
</div>
<a
@@ -137,24 +124,6 @@ export class DialogManageCloudhook extends LitElement {
}
}
private _focusInput(ev) {
const inputElement = ev.currentTarget as HaTextField;
inputElement.select();
}
private async _copyUrl(ev): Promise<void> {
if (!this.hass) return;
ev.stopPropagation();
const inputElement = ev.target.parentElement as HaTextField;
inputElement.select();
const url = this.hass.hassUrl(inputElement.value);
await copyToClipboard(url);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -163,13 +132,6 @@ export class DialogManageCloudhook extends LitElement {
ha-dialog {
width: 650px;
}
ha-textfield {
display: block;
}
ha-textfield > ha-icon-button {
--mdc-icon-button-size: 24px;
--mdc-icon-size: 18px;
}
button.link {
color: var(--primary-color);
text-decoration: none;
@@ -28,6 +28,7 @@ import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
import { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package";
import { showCloudAlreadyConnectedDialog } from "../dialog-cloud-already-connected/show-dialog-cloud-already-connected";
@customElement("cloud-login")
export class CloudLogin extends LitElement {
@@ -47,6 +48,8 @@ export class CloudLogin extends LitElement {
@state() private _error?: string;
@state() private _checkConnection = true;
@query("#email", true) private _emailField!: HaTextField;
@query("#password", true) private _passwordField!: HaPasswordField;
@@ -244,6 +247,7 @@ export class CloudLogin extends LitElement {
hass: this.hass,
email: username,
...(code ? { code } : { password }),
check_connection: this._checkConnection,
});
this.email = "";
this._password = "";
@@ -283,6 +287,21 @@ export class CloudLogin extends LitElement {
return;
}
}
if (errCode === "alreadyconnectederror") {
showCloudAlreadyConnectedDialog(this, {
details: JSON.parse(err.body.message),
logInHereAction: () => {
this._checkConnection = false;
doLogin(username);
},
closeDialog: () => {
this._requestInProgress = false;
this.email = "";
this._password = "";
},
});
return;
}
if (errCode === "PasswordChangeRequired") {
showAlertDialog(this, {
title: this.hass.localize(
@@ -91,7 +91,7 @@ class DialogMQTTDeviceDebugInfo extends LitElement {
? this._renderEntities()
: html`
${this.hass!.localize(
"ui.dialogs.mqtt_device_debug_info.no_entities"
"ui.dialogs.mqtt_device_debug_info.no_entity_debug_info"
)}
`}
</ul>
@@ -103,7 +103,7 @@ class DialogMQTTDeviceDebugInfo extends LitElement {
? this._renderTriggers()
: html`
${this.hass!.localize(
"ui.dialogs.mqtt_device_debug_info.no_triggers"
"ui.dialogs.mqtt_device_debug_info.no_trigger_debug_info"
)}
`}
</ul>
@@ -1,5 +1,4 @@
import { consume } from "@lit-labs/context";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import {
mdiCog,
mdiDelete,
@@ -30,6 +29,7 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-svg-icon";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-tooltip";
import { getSignedPath } from "../../../data/auth";
import type {
ConfigEntry,
@@ -444,7 +444,13 @@ export class HaConfigDevicePage extends LitElement {
${this._related.automation.map((automation) => {
const entityState = this.hass.states[automation];
return entityState
? html`<div>
? html`<ha-tooltip
placement="left"
.disabled=${!!entityState.attributes.id}
.content=${this.hass.localize(
"ui.panel.config.devices.cant_edit"
)}
>
<a
href=${ifDefined(
entityState.attributes.id
@@ -461,17 +467,8 @@ export class HaConfigDevicePage extends LitElement {
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
${!entityState.attributes.id
? html`
<simple-tooltip animation-delay="0">
${this.hass.localize(
"ui.panel.config.devices.cant_edit"
)}
</simple-tooltip>
`
: ""}
</div> `
: "";
</ha-tooltip>`
: nothing;
})}
</div>
`
@@ -536,7 +533,13 @@ export class HaConfigDevicePage extends LitElement {
const entityState = this.hass.states[scene];
return entityState
? html`
<div>
<ha-tooltip
placement="left"
.disabled=${!!entityState.attributes.id}
.content=${this.hass.localize(
"ui.panel.config.devices.cant_edit"
)}
>
<a
href=${ifDefined(
entityState.attributes.id
@@ -553,18 +556,9 @@ export class HaConfigDevicePage extends LitElement {
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
${!entityState.attributes.id
? html`
<simple-tooltip animation-delay="0">
${this.hass.localize(
"ui.panel.config.devices.cant_edit"
)}
</simple-tooltip>
`
: ""}
</div>
</ha-tooltip>
`
: "";
: nothing;
})}
</div>
`
@@ -1,5 +1,4 @@
import { consume } from "@lit-labs/context";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import {
mdiChevronRight,
mdiDotsVertical,
@@ -1,5 +1,4 @@
import { consume } from "@lit-labs/context";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import {
mdiAlertCircle,
mdiCancel,
@@ -66,6 +65,7 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
import { getConfigEntries, getSubEntries } from "../../../data/config_entries";
import { fullEntitiesContext } from "../../../data/context";
@@ -367,22 +367,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
tabindex="0"
style="display:inline-block; position: relative;"
>
<ha-svg-icon
style=${styleMap({
color: entry.unavailable ? "var(--error-color)" : "",
})}
.path=${entry.restored
? mdiRestoreAlert
: entry.unavailable
? mdiAlertCircle
: entry.disabled_by
? mdiCancel
: entry.hidden_by
? mdiEyeOff
: mdiPencilOff}
></ha-svg-icon>
<simple-tooltip animation-delay="0" position="left">
${entry.restored
<ha-tooltip
placement="left"
.content=${entry.restored
? this.hass.localize(
"ui.panel.config.entities.picker.status.not_provided"
)
@@ -401,7 +388,22 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
: this.hass.localize(
"ui.panel.config.entities.picker.status.unmanageable"
)}
</simple-tooltip>
>
<ha-svg-icon
style=${styleMap({
color: entry.unavailable ? "var(--error-color)" : "",
})}
.path=${entry.restored
? mdiRestoreAlert
: entry.unavailable
? mdiAlertCircle
: entry.disabled_by
? mdiCancel
: entry.hidden_by
? mdiEyeOff
: mdiPencilOff}
></ha-svg-icon>
</ha-tooltip>
</div>
`
: "—",
@@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -10,6 +9,7 @@ import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import "../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-list-item";
import "../../../components/ha-tooltip";
import { getConfigFlowHandlers } from "../../../data/config_flow";
import { createCounter } from "../../../data/counter";
import { createInputBoolean } from "../../../data/input_boolean";
@@ -210,39 +210,39 @@ export class DialogHelperDetail extends LitElement {
const isLoaded =
!(domain in HELPERS) || isComponentLoaded(this.hass, domain);
return html`
<ha-list-item
.disabled=${!isLoaded}
hasmeta
.domain=${domain}
@request-selected=${this._domainPicked}
graphic="icon"
<ha-tooltip
.disabled=${isLoaded}
.content=${this.hass.localize(
"ui.dialogs.helper_settings.platform_not_loaded",
{ platform: domain }
)}
>
<img
slot="graphic"
loading="lazy"
alt=""
src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<span class="item-text"> ${label} </span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
${!isLoaded
? html`
<simple-tooltip animation-delay="0"
>${this.hass.localize(
"ui.dialogs.helper_settings.platform_not_loaded",
{ platform: domain }
)}</simple-tooltip
>
`
: ""}
<div>
<ha-list-item
.disabled=${!isLoaded}
hasmeta
.domain=${domain}
@request-selected=${this._domainPicked}
graphic="icon"
>
<img
slot="graphic"
loading="lazy"
alt=""
src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<span class="item-text"> ${label} </span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</div>
</ha-tooltip>
`;
})}
</mwc-list>
@@ -1,6 +1,5 @@
import { consume } from "@lit-labs/context";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import {
mdiAlertCircle,
mdiCancel,
@@ -52,6 +51,7 @@ import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import type { CategoryRegistryEntry } from "../../../data/category_registry";
import {
createCategoryRegistryEntry,
@@ -359,12 +359,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
tabindex="0"
style="display:inline-block; position: relative;"
>
<ha-svg-icon .path=${mdiPencilOff}></ha-svg-icon>
<simple-tooltip animation-delay="0" position="left">
${this.hass.localize(
<ha-tooltip
placement="left"
.content=${this.hass.localize(
"ui.panel.config.entities.picker.status.unmanageable"
)}
</simple-tooltip>
>
<ha-svg-icon .path=${mdiPencilOff}></ha-svg-icon>
</ha-tooltip>
</div>
`
: ""}

Some files were not shown because too many files have changed in this diff Show More