Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d0cdfbde1 | ||
|
|
97f8aac984 | ||
|
|
6d9c7eb52a | ||
|
|
311143d6a1 | ||
|
|
b9efdfa10e | ||
|
|
b64042186f | ||
|
|
cf288f7cd1 | ||
|
|
1c8284609f | ||
|
|
1fdadbf1b8 | ||
|
|
3b272ae411 | ||
|
|
a048c36861 | ||
|
|
2a6c1773f3 | ||
|
|
bf17012753 | ||
|
|
bbaf23e049 | ||
|
|
34b7929165 | ||
|
|
8f06e70a11 | ||
|
|
50ac60b35e | ||
|
|
4caca19e32 | ||
|
|
a7b1c45c00 | ||
|
|
10c3e4c6f8 | ||
|
|
1f50c359dc | ||
|
|
a3e24a3dc0 | ||
|
|
a906285a03 | ||
|
|
5c933a43b2 | ||
|
|
151a7fbc40 | ||
|
|
4fa915c869 | ||
|
|
d47e5c847b | ||
|
|
db5036aed3 | ||
|
|
bb672d0272 | ||
|
|
e26d3d39f0 | ||
|
|
e54c3a69af | ||
|
|
cc04457d72 | ||
|
|
9e1d64e728 | ||
|
|
0cfe7f8d12 | ||
|
|
2b1f301db6 | ||
|
|
fc4996412e | ||
|
|
ece4a6345f | ||
|
|
a4c08a78b9 | ||
|
|
a438fc5e41 | ||
|
|
783132ae46 | ||
|
|
680d81001c | ||
|
|
a917383d7a | ||
|
|
455a6761cd | ||
|
|
acf42d7637 | ||
|
|
3857c7321a | ||
|
|
5eec814988 | ||
|
|
edd37565a6 | ||
|
|
fb3f779121 | ||
|
|
4d7634ac67 | ||
|
|
ba5c1133c6 | ||
|
|
0a05dd8f71 | ||
|
|
400106ec09 | ||
|
|
a7a4194e09 | ||
|
|
0bd7d27c57 | ||
|
|
8175e45921 | ||
|
|
cae36b393b | ||
|
|
f84ad92356 | ||
|
|
fb1ee2ed1d | ||
|
|
9073282174 | ||
|
|
91bd5cba08 | ||
|
|
a68bdbfe08 | ||
|
|
f3d614b0d3 | ||
|
|
f3c9e4a4a0 | ||
|
|
d22a82c4a6 | ||
|
|
5cddc6e5c6 | ||
|
|
c5c067ef19 | ||
|
|
694bb3088c | ||
|
|
ad487470fd |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/")
|
||||
|
||||
@@ -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";
|
||||
@@ -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() {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 |
@@ -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,2 +1,2 @@
|
||||
export const computeDomain = (entityId: string): string =>
|
||||
entityId.substr(0, entityId.indexOf("."));
|
||||
entityId.substring(0, entityId.indexOf("."));
|
||||
|
||||
@@ -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")
|
||||
) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface CloudWebhook {
|
||||
interface CloudLoginBase {
|
||||
hass: HomeAssistant;
|
||||
email: string;
|
||||
check_connection?: boolean;
|
||||
}
|
||||
|
||||
export interface CloudLoginPassword extends CloudLoginBase {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
: ""}
|
||||
|
||||