Compare commits
2 Commits
spacing-ca
...
websocket_
Author | SHA1 | Date | |
---|---|---|---|
![]() |
dd00b51f21 | ||
![]() |
64b886eea0 |
@@ -5,15 +5,12 @@
|
||||
"context": ".."
|
||||
},
|
||||
"appPort": "8124:8123",
|
||||
"postCreateCommand": "./.devcontainer/post_create.sh",
|
||||
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
|
||||
"postStartCommand": "script/bootstrap",
|
||||
"containerEnv": {
|
||||
"DEV_CONTAINER": "1",
|
||||
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
||||
},
|
||||
"remoteEnv": {
|
||||
"NODE_OPTIONS": "--max_old_space_size=8192"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
|
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script will run after the container is created
|
||||
|
||||
# add github cli
|
||||
(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \
|
||||
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||
&& cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
|
||||
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
||||
|
||||
# Update package lists
|
||||
sudo apt-get update
|
||||
|
||||
sudo apt upgrade -y
|
||||
|
||||
# Install necessary packages
|
||||
sudo apt-get install -y libpcap-dev gh
|
||||
|
||||
# Display a message
|
||||
echo "Post-create script has been executed successfully."
|
6
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@v4.2.1
|
||||
uses: actions/cache@v4.2.0
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
|
4
.github/workflows/nightly.yaml
vendored
@@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
42
.vscode/tasks.json
vendored
@@ -1,42 +1,6 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Develop and serve Frontend",
|
||||
"type": "shell",
|
||||
"command": "script/develop_and_serve -c ${input:coreUrl}",
|
||||
// Sync changes here to other tasks until issue resolved
|
||||
// https://github.com/Microsoft/vscode/issues/61497
|
||||
"problemMatcher": {
|
||||
"owner": "ha-build",
|
||||
"source": "ha-build",
|
||||
"fileLocation": "absolute",
|
||||
"severity": "error",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
|
||||
"severity": 1,
|
||||
"file": 2,
|
||||
"message": 3,
|
||||
"line": 4,
|
||||
"column": 5
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "Changes detected. Starting compilation",
|
||||
"endsPattern": "Build done @"
|
||||
}
|
||||
},
|
||||
"isBackground": true,
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"runOptions": {
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Develop Frontend",
|
||||
"type": "gulp",
|
||||
@@ -277,12 +241,6 @@
|
||||
"id": "supervisorToken",
|
||||
"type": "promptString",
|
||||
"description": "The token for the Remote API proxy add-on"
|
||||
},
|
||||
{
|
||||
"id": "coreUrl",
|
||||
"type": "promptString",
|
||||
"description": "The URL of the Home Assistant Core instance",
|
||||
"default": "http://127.0.0.1:8123"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -1,16 +1,16 @@
|
||||
// @ts-check
|
||||
|
||||
import tseslint from "typescript-eslint";
|
||||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
export default tseslint.config(...rootConfig, {
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"global-require": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"global-require": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
},
|
||||
},
|
||||
});
|
||||
];
|
||||
|
@@ -90,10 +90,6 @@ function copyMapPanel(staticDir) {
|
||||
npmPath("leaflet/dist/leaflet.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
copyFileDir(
|
||||
npmPath("leaflet.markercluster/dist/MarkerCluster.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
fs.copySync(
|
||||
npmPath("leaflet/dist/images"),
|
||||
staticPath("images/leaflet/images/")
|
||||
|
@@ -65,7 +65,6 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
config_subentry_id: null,
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
@@ -86,7 +85,6 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
},
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
config_subentry_id: null,
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
|
@@ -11,7 +11,6 @@ export const mockConfigEntries = (hass: MockHomeAssistant) => {
|
||||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
supports_reconfigure: true,
|
||||
supported_subentry_types: {},
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
disabled_by: null,
|
||||
|
@@ -1,16 +1,11 @@
|
||||
// @ts-check
|
||||
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import tseslint from "typescript-eslint";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import { configs as litConfigs } from "eslint-plugin-lit";
|
||||
import { configs as wcConfigs } from "eslint-plugin-wc";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = path.dirname(_filename);
|
||||
@@ -20,14 +15,17 @@ const compat = new FlatCompat({
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default tseslint.config(
|
||||
...compat.extends("airbnb-base", "plugin:lit-a11y/recommended"),
|
||||
eslintConfigPrettier,
|
||||
litConfigs["flat/all"],
|
||||
tseslint.configs.recommended,
|
||||
tseslint.configs.strict,
|
||||
tseslint.configs.stylistic,
|
||||
wcConfigs["flat/recommended"],
|
||||
export default [
|
||||
...compat.extends(
|
||||
"airbnb-base",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/strict",
|
||||
"plugin:@typescript-eslint/stylistic",
|
||||
"plugin:wc/recommended",
|
||||
"plugin:lit/all",
|
||||
"plugin:lit-a11y/recommended",
|
||||
"prettier"
|
||||
),
|
||||
{
|
||||
plugins: {
|
||||
"unused-imports": unusedImports,
|
||||
@@ -45,7 +43,7 @@ export default tseslint.config(
|
||||
Polymer: true,
|
||||
},
|
||||
|
||||
parser: tseslint.parser,
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
|
||||
@@ -186,5 +184,5 @@ export default tseslint.config(
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
];
|
||||
|
@@ -1,10 +1,10 @@
|
||||
// @ts-check
|
||||
|
||||
import tseslint from "typescript-eslint";
|
||||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
export default tseslint.config(...rootConfig, {
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
},
|
||||
},
|
||||
});
|
||||
];
|
||||
|
@@ -1,10 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,7 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 964 B |
@@ -48,7 +48,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -72,7 +71,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -96,7 +94,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
|
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Select box
|
||||
---
|
@@ -1,152 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -47,7 +47,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -71,7 +70,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -95,7 +93,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
|
@@ -1,30 +0,0 @@
|
||||
---
|
||||
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)
|
@@ -1,2 +0,0 @@
|
||||
import "../../../../src/components/ha-tooltip";
|
||||
import "../../../../src/components/ha-button";
|
@@ -32,8 +32,6 @@ const createConfigEntry = (
|
||||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
supports_reconfigure: true,
|
||||
supported_subentry_types: {},
|
||||
num_subentries: 0,
|
||||
disabled_by: null,
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
@@ -190,7 +188,6 @@ const createEntityRegistryEntries = (
|
||||
): EntityRegistryEntry[] => [
|
||||
{
|
||||
config_entry_id: item.entry_id,
|
||||
config_subentry_id: null,
|
||||
device_id: "mock-device-id",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
@@ -217,7 +214,6 @@ const createDeviceRegistryEntries = (
|
||||
{
|
||||
entry_type: null,
|
||||
config_entries: [item.entry_id],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
manufacturer: "ESPHome",
|
||||
model: "Mock Device",
|
||||
|
@@ -253,9 +253,13 @@ 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,8 @@
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import { stripDiacritics } from "../../../src/common/string/strip-diacritics";
|
||||
import type { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { getStripDiacriticsFn } from "../../../src/util/fuse";
|
||||
|
||||
export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
const options: IFuseOptions<StoreAddon> = {
|
||||
@@ -8,8 +10,8 @@ export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
ignoreDiacritics: true,
|
||||
getFn: getStripDiacriticsFn,
|
||||
};
|
||||
const fuse = new Fuse(addons, options);
|
||||
return fuse.search(filter).map((result) => result.item);
|
||||
return fuse.search(stripDiacritics(filter)).map((result) => result.item);
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"hassio-backup-uploaded": { backup: HassioBackup };
|
||||
"backup-uploaded": { backup: HassioBackup };
|
||||
"backup-cleared": undefined;
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export class HassioUploadBackup extends LitElement {
|
||||
this._uploading = true;
|
||||
try {
|
||||
const backup = await uploadBackup(this.hass, file);
|
||||
fireEvent(this, "hassio-backup-uploaded", { backup: backup.data });
|
||||
fireEvent(this, "backup-uploaded", { backup: backup.data });
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: "Upload failed",
|
||||
|
@@ -5,6 +5,7 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { formatDate } from "../../../src/common/datetime/format_date";
|
||||
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
||||
import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-textfield";
|
||||
@@ -18,10 +19,13 @@ import type {
|
||||
} from "../../../src/data/hassio/backup";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import type { HomeAssistant, TranslationDict } from "../../../src/types";
|
||||
import "./supervisor-formfield-label";
|
||||
import type { HaTextField } from "../../../src/components/ha-textfield";
|
||||
|
||||
type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
||||
|
||||
interface CheckboxItem {
|
||||
slug: string;
|
||||
checked: boolean;
|
||||
@@ -63,6 +67,8 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
|
||||
export class SupervisorBackupContent extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public backup?: HassioBackupDetail;
|
||||
@@ -109,6 +115,10 @@ export class SupervisorBackupContent extends LitElement {
|
||||
this._focusTarget?.focus();
|
||||
}
|
||||
|
||||
private _localize = (key: BackupOrRestoreKey) =>
|
||||
this.supervisor?.localize(`backup.${key}`) ||
|
||||
this.localize!(`ui.panel.page-onboarding.restore.${key}`);
|
||||
|
||||
protected render() {
|
||||
if (!this.onboarding && !this.supervisor) {
|
||||
return nothing;
|
||||
@@ -122,8 +132,8 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${this.backup
|
||||
? html`<div class="details">
|
||||
${this.backup.type === "full"
|
||||
? this.supervisor?.localize("backup.full_backup")
|
||||
: this.supervisor?.localize("backup.partial_backup")}
|
||||
? this._localize("full_backup")
|
||||
: this._localize("partial_backup")}
|
||||
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
||||
${this.hass
|
||||
? formatDateTime(
|
||||
@@ -135,7 +145,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
</div>`
|
||||
: html`<ha-textfield
|
||||
name="backupName"
|
||||
.label=${this.supervisor?.localize("backup.name")}
|
||||
.label=${this._localize("name")}
|
||||
.value=${this.backupName}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
@@ -143,13 +153,11 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${!this.backup || this.backup.type === "full"
|
||||
? html`<div class="sub-header">
|
||||
${!this.backup
|
||||
? this.supervisor?.localize("backup.type")
|
||||
: this.supervisor?.localize("backup.select_type")}
|
||||
? this._localize("type")
|
||||
: this._localize("select_type")}
|
||||
</div>
|
||||
<div class="backup-types">
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.full_backup")}
|
||||
>
|
||||
<ha-formfield .label=${this._localize("full_backup")}>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="full"
|
||||
@@ -158,9 +166,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.partial_backup")}
|
||||
>
|
||||
<ha-formfield .label=${this._localize("partial_backup")}>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="partial"
|
||||
@@ -196,7 +202,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this.supervisor?.localize("backup.folders")}
|
||||
.label=${this._localize("folders")}
|
||||
.iconPath=${mdiFolder}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
@@ -216,7 +222,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this.supervisor?.localize("backup.addons")}
|
||||
.label=${this._localize("addons")}
|
||||
.iconPath=${mdiPuzzle}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
@@ -241,7 +247,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${!this.backup
|
||||
? html`<ha-formfield
|
||||
class="password"
|
||||
.label=${this.supervisor?.localize("backup.password_protection")}
|
||||
.label=${this._localize("password_protection")}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.backupHasPassword}
|
||||
@@ -253,7 +259,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${this.backupHasPassword
|
||||
? html`
|
||||
<ha-password-field
|
||||
.label=${this.supervisor?.localize("backup.password")}
|
||||
.label=${this._localize("password")}
|
||||
name="backupPassword"
|
||||
.value=${this.backupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
@@ -261,7 +267,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
</ha-password-field>
|
||||
${!this.backup
|
||||
? html`<ha-password-field
|
||||
.label=${this.supervisor?.localize("backup.confirm_password")}
|
||||
.label=${this._localize("confirm_password")}
|
||||
name="confirmBackupPassword"
|
||||
.value=${this.confirmBackupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
|
@@ -72,7 +72,7 @@ export class DialogHassioBackupUpload
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
<hassio-upload-backup
|
||||
@hassio-backup-uploaded=${this._backupUploaded}
|
||||
@backup-uploaded=${this._backupUploaded}
|
||||
.hass=${this.hass}
|
||||
></hassio-upload-backup>
|
||||
</ha-dialog>
|
||||
|
@@ -35,6 +35,7 @@ import { fileDownload } from "../../../../src/util/file_download";
|
||||
import "../../components/supervisor-backup-content";
|
||||
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
||||
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
|
||||
import type { BackupOrRestoreKey } from "../../util/translations";
|
||||
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
|
||||
|
||||
@customElement("dialog-hassio-backup")
|
||||
@@ -42,7 +43,7 @@ class HassioBackupDialog
|
||||
extends LitElement
|
||||
implements HassDialog<HassioBackupDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@@ -61,13 +62,9 @@ class HassioBackupDialog
|
||||
this._dialogParams = dialogParams;
|
||||
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
|
||||
if (!this._backup) {
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.no_backup_found"
|
||||
);
|
||||
this._error = this._localize("no_backup_found");
|
||||
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.restore_no_home_assistant"
|
||||
);
|
||||
this._error = this._localize("restore_no_home_assistant");
|
||||
}
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
@@ -85,6 +82,13 @@ class HassioBackupDialog
|
||||
return true;
|
||||
}
|
||||
|
||||
private _localize(key: BackupOrRestoreKey) {
|
||||
return (
|
||||
this._dialogParams!.supervisor?.localize(`backup.${key}`) ||
|
||||
this._dialogParams!.localize!(`ui.panel.page-onboarding.restore.${key}`)
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams || !this._backup) {
|
||||
return nothing;
|
||||
@@ -98,7 +102,7 @@ class HassioBackupDialog
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this._dialogParams.supervisor?.localize("backup.close")}
|
||||
.label=${this._localize("close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._restoringBackup}
|
||||
@@ -146,6 +150,7 @@ class HassioBackupDialog
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
.backup=${this._backup}
|
||||
.onboarding=${this._dialogParams.onboarding || false}
|
||||
.localize=${this._dialogParams.localize}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</supervisor-backup-content>
|
||||
@@ -156,7 +161,7 @@ class HassioBackupDialog
|
||||
.disabled=${this._restoringBackup || !!this._error}
|
||||
@click=${this._restoreClicked}
|
||||
>
|
||||
${this._dialogParams.supervisor?.localize("backup.restore")}
|
||||
${this._localize("restore")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
@@ -191,22 +196,18 @@ class HassioBackupDialog
|
||||
}
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_title"
|
||||
: "confirm_restore_partial_backup_title"
|
||||
}`
|
||||
title: this._localize(
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_title"
|
||||
: "confirm_restore_partial_backup_title"
|
||||
),
|
||||
text: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_text"
|
||||
: "confirm_restore_partial_backup_text"
|
||||
}`
|
||||
text: this._localize(
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_text"
|
||||
: "confirm_restore_partial_backup_text"
|
||||
),
|
||||
confirmText: supervisor?.localize("backup.restore"),
|
||||
dismissText: supervisor?.localize("backup.cancel"),
|
||||
confirmText: this._localize("restore"),
|
||||
dismissText: this._localize("cancel"),
|
||||
}))
|
||||
) {
|
||||
this._restoringBackup = false;
|
||||
@@ -226,8 +227,7 @@ class HassioBackupDialog
|
||||
this.closeDialog();
|
||||
} catch (error: any) {
|
||||
this._error =
|
||||
error?.body?.message ||
|
||||
supervisor?.localize("backup.restore_start_failed");
|
||||
error?.body?.message || this._localize("restore_start_failed");
|
||||
} finally {
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
@@ -286,7 +286,7 @@ class HassioBackupDialog
|
||||
title: supervisor.localize("backup.remote_download_title"),
|
||||
text: supervisor.localize("backup.remote_download_text"),
|
||||
confirmText: supervisor.localize("backup.download"),
|
||||
dismissText: supervisor?.localize("backup.cancel"),
|
||||
dismissText: this._localize("cancel"),
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
@@ -302,7 +302,7 @@ class HassioBackupDialog
|
||||
private get _computeName() {
|
||||
return this._backup
|
||||
? this._backup.name || this._backup.slug
|
||||
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
|
||||
: this._localize("unnamed_backup");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../src/common/translations/localize";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioBackupDialogParams {
|
||||
@@ -7,6 +8,7 @@ export interface HassioBackupDialogParams {
|
||||
onRestoring?: () => void;
|
||||
onboarding?: boolean;
|
||||
supervisor?: Supervisor;
|
||||
localize?: LocalizeFunc;
|
||||
}
|
||||
|
||||
export const showHassioBackupDialog = (
|
||||
|
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -7,7 +8,6 @@ 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,27 +118,28 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
<div>${repo.maintainer}</div>
|
||||
<div>${repo.url}</div>
|
||||
</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>
|
||||
<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-md-list-item>
|
||||
`
|
||||
)
|
||||
|
4
hassio/src/util/translations.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { TranslationDict } from "../../../src/types";
|
||||
|
||||
export type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
@@ -17,7 +17,6 @@ import "../../../src/components/ha-alert";
|
||||
import {
|
||||
ALTERNATIVE_DNS_SERVERS,
|
||||
getSupervisorNetworkInfo,
|
||||
pingSupervisor,
|
||||
setSupervisorNetworkDns,
|
||||
} from "../data/supervisor";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
@@ -86,28 +85,7 @@ class LandingPageNetwork extends LitElement {
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
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();
|
||||
}
|
||||
this._fetchSupervisorInfo();
|
||||
}
|
||||
|
||||
private _scheduleFetchSupervisorInfo() {
|
||||
|
@@ -18,7 +18,7 @@ export const ALTERNATIVE_DNS_SERVERS: {
|
||||
];
|
||||
|
||||
export async function getSupervisorLogs(lines = 100) {
|
||||
return fetch(`/supervisor-api/supervisor/logs?lines=${lines}`, {
|
||||
return fetch(`/supervisor/supervisor/logs?lines=${lines}`, {
|
||||
headers: {
|
||||
Accept: "text/plain",
|
||||
},
|
||||
@@ -26,26 +26,22 @@ export async function getSupervisorLogs(lines = 100) {
|
||||
}
|
||||
|
||||
export async function getSupervisorLogsFollow(lines = 500) {
|
||||
return fetch(`/supervisor-api/supervisor/logs/follow?lines=${lines}`, {
|
||||
return fetch(`/supervisor/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-api/network/info");
|
||||
return fetch("/supervisor/network/info");
|
||||
}
|
||||
|
||||
export const setSupervisorNetworkDns = async (
|
||||
dnsServerIndex: number,
|
||||
primaryInterface: string
|
||||
) =>
|
||||
fetch(`/supervisor-api/network/interface/${primaryInterface}/update`, {
|
||||
fetch(`/supervisor/network/interface/${primaryInterface}/update`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
ipv4: {
|
||||
|
91
package.json
@@ -26,25 +26,25 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.26.9",
|
||||
"@babel/runtime": "7.26.7",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/autocomplete": "6.18.4",
|
||||
"@codemirror/commands": "6.8.0",
|
||||
"@codemirror/language": "6.10.8",
|
||||
"@codemirror/legacy-modes": "6.4.3",
|
||||
"@codemirror/search": "6.5.9",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.36.3",
|
||||
"@codemirror/legacy-modes": "6.4.2",
|
||||
"@codemirror/search": "6.5.8",
|
||||
"@codemirror/state": "6.5.1",
|
||||
"@codemirror/view": "6.36.2",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.17.3",
|
||||
"@formatjs/intl-displaynames": "6.8.10",
|
||||
"@formatjs/intl-durationformat": "0.7.3",
|
||||
"@formatjs/intl-datetimeformat": "6.17.2",
|
||||
"@formatjs/intl-displaynames": "6.8.9",
|
||||
"@formatjs/intl-durationformat": "0.7.2",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.4",
|
||||
"@formatjs/intl-listformat": "7.7.10",
|
||||
"@formatjs/intl-locale": "4.2.10",
|
||||
"@formatjs/intl-numberformat": "8.15.3",
|
||||
"@formatjs/intl-pluralrules": "5.4.3",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.10",
|
||||
"@formatjs/intl-listformat": "7.7.9",
|
||||
"@formatjs/intl-locale": "4.2.9",
|
||||
"@formatjs/intl-numberformat": "8.15.2",
|
||||
"@formatjs/intl-pluralrules": "5.4.2",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.9",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
@@ -53,9 +53,10 @@
|
||||
"@fullcalendar/timegrid": "6.1.15",
|
||||
"@lezer/highlight": "1.2.1",
|
||||
"@lit-labs/context": "0.4.1",
|
||||
"@lit-labs/motion": "1.0.8",
|
||||
"@lit-labs/observers": "2.0.5",
|
||||
"@lit-labs/virtualizer": "2.1.0",
|
||||
"@lit-labs/motion": "1.0.7",
|
||||
"@lit-labs/observers": "2.0.4",
|
||||
"@lit-labs/virtualizer": "2.0.15",
|
||||
"@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",
|
||||
@@ -89,16 +90,15 @@
|
||||
"@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",
|
||||
"@vaadin/combo-box": "24.6.2",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.2",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"barcode-detector": "3.0.0",
|
||||
"barcode-detector": "2.3.1",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.40.0",
|
||||
@@ -110,24 +110,22 @@
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "5.6.0",
|
||||
"element-internals-polyfill": "1.3.13",
|
||||
"fuse.js": "7.1.0",
|
||||
"fuse.js": "7.0.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.4.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.7.15",
|
||||
"intl-messageformat": "10.7.14",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "2.8.0",
|
||||
"lit-html": "2.8.0",
|
||||
"luxon": "3.5.0",
|
||||
"marked": "15.0.7",
|
||||
"marked": "15.0.6",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"object-hash": "3.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"qr-scanner": "1.4.2",
|
||||
"qrcode": "1.5.4",
|
||||
@@ -139,7 +137,7 @@
|
||||
"tinykeys": "3.0.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "2.0.2",
|
||||
"ua-parser-js": "2.0.0",
|
||||
"vis-data": "7.1.9",
|
||||
"vis-network": "9.1.9",
|
||||
"vue": "2.7.16",
|
||||
@@ -154,22 +152,22 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.9",
|
||||
"@babel/core": "7.26.7",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.3",
|
||||
"@babel/plugin-proposal-decorators": "7.25.9",
|
||||
"@babel/plugin-transform-runtime": "7.26.9",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@babel/plugin-transform-runtime": "7.25.9",
|
||||
"@babel/preset-env": "7.26.7",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.18.2",
|
||||
"@lokalise/node-api": "13.2.1",
|
||||
"@octokit/auth-oauth-device": "7.1.3",
|
||||
"@octokit/plugin-retry": "7.1.4",
|
||||
"@octokit/rest": "21.1.1",
|
||||
"@lokalise/node-api": "13.0.0",
|
||||
"@octokit/auth-oauth-device": "7.1.2",
|
||||
"@octokit/plugin-retry": "7.1.3",
|
||||
"@octokit/rest": "21.1.0",
|
||||
"@rsdoctor/rspack-plugin": "0.4.13",
|
||||
"@rspack/cli": "1.2.5",
|
||||
"@rspack/core": "1.2.5",
|
||||
"@rspack/cli": "1.2.2",
|
||||
"@rspack/core": "1.2.2",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.21",
|
||||
"@types/chromecast-caf-receiver": "6.0.20",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/glob": "8.1.0",
|
||||
@@ -177,7 +175,6 @@
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.16",
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/leaflet.markercluster": "1.5.5",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/mocha": "10.0.10",
|
||||
@@ -186,12 +183,14 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "3.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.21.0",
|
||||
"@typescript-eslint/parser": "8.21.0",
|
||||
"@vitest/coverage-v8": "3.0.4",
|
||||
"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.21.0",
|
||||
"eslint": "9.19.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -199,7 +198,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.1",
|
||||
"eslint-plugin-wc": "2.2.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.0",
|
||||
"glob": "11.0.1",
|
||||
@@ -216,18 +215,16 @@
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"object-hash": "3.0.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.5.2",
|
||||
"prettier": "3.4.2",
|
||||
"rspack-manifest-plugin": "5.0.3",
|
||||
"serve": "14.2.4",
|
||||
"sinon": "19.0.2",
|
||||
"tar": "7.4.3",
|
||||
"terser-webpack-plugin": "5.3.11",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.24.1",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.0.6",
|
||||
"vitest": "3.0.4",
|
||||
"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"
|
||||
@@ -241,7 +238,7 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"globals": "16.0.0",
|
||||
"globals": "15.14.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0"
|
||||
|
@@ -1,10 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,10 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,7 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 964 B |
@@ -1,7 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 964 B |
@@ -1,7 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 652 B |
@@ -1,7 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 654 B |
@@ -1,7 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 699 B |
@@ -1,7 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 701 B |
@@ -1,6 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 414 B |
@@ -1,6 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 416 B |
@@ -1,6 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 414 B |
@@ -1,6 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 416 B |
@@ -1,11 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,11 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1,11 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,11 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1,11 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,11 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1,24 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 4.5 KiB |
@@ -1,17 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -1,9 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,11 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20250205.0"
|
||||
version = "20250129.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -64,7 +64,7 @@ echo Core is used from ${coreUrl}
|
||||
HASS_URL="$coreUrl" ./script/develop &
|
||||
|
||||
# serve the frontend
|
||||
./node_modules/.bin/serve -p $frontendPort --single --no-port-switching --config ../script/serve-config.json ./hass_frontend &
|
||||
yarn dlx serve -l $frontendPort ./hass_frontend -s &
|
||||
|
||||
# keep the script running while serving
|
||||
wait
|
||||
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"cleanUrls": false
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { theme2hex } from "./convert-color";
|
||||
|
||||
export const COLORS = [
|
||||
@@ -75,12 +74,3 @@ export function getGraphColorByIndex(
|
||||
getColorByIndex(index);
|
||||
return theme2hex(themeColor);
|
||||
}
|
||||
|
||||
export const getAllGraphColors = memoizeOne(
|
||||
(style: CSSStyleDeclaration) =>
|
||||
COLORS.map((_color, index) => getGraphColorByIndex(index, style)),
|
||||
(newArgs: [CSSStyleDeclaration], lastArgs: [CSSStyleDeclaration]) =>
|
||||
// this is not ideal, but we need to memoize the colors
|
||||
newArgs[0].getPropertyValue("--graph-color-1") ===
|
||||
lastArgs[0].getPropertyValue("--graph-color-1")
|
||||
);
|
||||
|
@@ -136,18 +136,11 @@ export function theme2hex(themeColor: string): string {
|
||||
}
|
||||
|
||||
const rgbFromColorName = colors[themeColor];
|
||||
if (rgbFromColorName) {
|
||||
return rgb2hex(rgbFromColorName);
|
||||
if (!rgbFromColorName) {
|
||||
// We have a named color, and there's nothing in the table,
|
||||
// so nothing further we can do with it.
|
||||
// Compare/border/background color will all be the same.
|
||||
return themeColor;
|
||||
}
|
||||
|
||||
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
||||
if (rgbMatch) {
|
||||
const [, r, g, b] = rgbMatch.map(Number);
|
||||
return rgb2hex([r, g, b]);
|
||||
}
|
||||
|
||||
// We have a named color, and there's nothing in the table,
|
||||
// so nothing further we can do with it.
|
||||
// Compare/border/background color will all be the same.
|
||||
return themeColor;
|
||||
return rgb2hex(rgbFromColorName);
|
||||
}
|
||||
|
@@ -26,20 +26,6 @@ const formatDateTimeMem = memoizeOne(
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDateTimeWithBrowserDefaults = (dateObj: Date) =>
|
||||
formatDateTimeWithBrowserDefaultsMem().format(dateObj);
|
||||
|
||||
const formatDateTimeWithBrowserDefaultsMem = memoizeOne(
|
||||
() =>
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
|
||||
// Aug 9, 2021, 8:23 AM
|
||||
export const formatShortDateTimeWithYear = (
|
||||
dateObj: Date,
|
||||
|
@@ -16,22 +16,11 @@ export const setupLeafletMap = async (
|
||||
const Leaflet = (await import("leaflet")).default as LeafletModuleType;
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
||||
|
||||
await import("leaflet.markercluster");
|
||||
|
||||
const map = Leaflet.map(mapElement);
|
||||
const style = document.createElement("link");
|
||||
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
|
||||
style.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(style);
|
||||
|
||||
const markerClusterStyle = document.createElement("link");
|
||||
markerClusterStyle.setAttribute(
|
||||
"href",
|
||||
"/static/images/leaflet/MarkerCluster.css"
|
||||
);
|
||||
markerClusterStyle.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(markerClusterStyle);
|
||||
|
||||
map.setView([52.3731339, 4.8903147], 13);
|
||||
|
||||
const tileLayer = createTileLayer(Leaflet).addTo(map);
|
||||
|
@@ -1,2 +1,2 @@
|
||||
export const computeDomain = (entityId: string): string =>
|
||||
entityId.substring(0, entityId.indexOf("."));
|
||||
entityId.substr(0, entityId.indexOf("."));
|
||||
|
@@ -120,6 +120,11 @@ 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`.
|
||||
@@ -176,7 +181,6 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
].includes(domain) ||
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
) {
|
||||
|
@@ -1,9 +1,6 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { stringCompare } from "../string/compare";
|
||||
|
||||
export const FIXED_DOMAIN_STATES = {
|
||||
alarm_control_panel: [
|
||||
@@ -240,7 +237,6 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
};
|
||||
|
||||
export const getStates = (
|
||||
hass: HomeAssistant,
|
||||
state: HassEntity,
|
||||
attribute: string | undefined = undefined
|
||||
): string[] => {
|
||||
@@ -273,19 +269,7 @@ export const getStates = (
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
if (!attribute) {
|
||||
result.push(
|
||||
...Object.entries(hass.states)
|
||||
.filter(
|
||||
([entityId, stateObj]) =>
|
||||
computeDomain(entityId) === "zone" &&
|
||||
entityId !== "zone.home" &&
|
||||
stateObj.attributes.friendly_name
|
||||
)
|
||||
.map(([_entityId, stateObj]) => stateObj.attributes.friendly_name!)
|
||||
.sort((zone1, zone2) =>
|
||||
stringCompare(zone1, zone2, hass.locale.language)
|
||||
)
|
||||
);
|
||||
result.push("home", "not_home");
|
||||
}
|
||||
break;
|
||||
case "event":
|
||||
|
@@ -1,32 +0,0 @@
|
||||
import type { LatLngExpression, Layer, Map, MarkerOptions } from "leaflet";
|
||||
import { Marker } from "leaflet";
|
||||
|
||||
export class DecoratedMarker extends Marker {
|
||||
decorationLayer: Layer | undefined;
|
||||
|
||||
constructor(
|
||||
latlng: LatLngExpression,
|
||||
decorationLayer?: Layer,
|
||||
options?: MarkerOptions
|
||||
) {
|
||||
super(latlng, options);
|
||||
|
||||
this.decorationLayer = decorationLayer;
|
||||
}
|
||||
|
||||
onAdd(map: Map) {
|
||||
super.onAdd(map);
|
||||
|
||||
// If decoration has been provided, add it to the map as well
|
||||
this.decorationLayer?.addTo(map);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
onRemove(map: Map) {
|
||||
// If decoration has been provided, remove it from the map as well
|
||||
this.decorationLayer?.remove();
|
||||
|
||||
return super.onRemove(map);
|
||||
}
|
||||
}
|
@@ -41,7 +41,7 @@ export class HaProgressButton extends LitElement {
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: nothing}
|
||||
: ""}
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
@@ -117,9 +117,6 @@ export class HaProgressButton extends LitElement {
|
||||
mwc-button.error slot {
|
||||
visibility: hidden;
|
||||
}
|
||||
:host([destructive]) {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { XAXisOption } from "echarts/types/dist/shared";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import {
|
||||
formatDateMonth,
|
||||
@@ -6,46 +7,56 @@ import {
|
||||
formatDateVeryShort,
|
||||
formatDateWeekdayShort,
|
||||
} from "../../common/datetime/format_date";
|
||||
import {
|
||||
formatTime,
|
||||
formatTimeWithSeconds,
|
||||
} from "../../common/datetime/format_time";
|
||||
import { formatTime } from "../../common/datetime/format_time";
|
||||
|
||||
export function formatTimeLabel(
|
||||
value: number | Date,
|
||||
export function getLabelFormatter(
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
minutesDifference: number
|
||||
dayDifference = 0
|
||||
) {
|
||||
const dayDifference = minutesDifference / 60 / 24;
|
||||
const date = new Date(value);
|
||||
if (dayDifference > 88) {
|
||||
return date.getMonth() === 0
|
||||
? `{bold|${formatDateMonthYear(date, locale, config)}}`
|
||||
: formatDateMonth(date, locale, config);
|
||||
}
|
||||
if (dayDifference > 35) {
|
||||
return date.getDate() === 1
|
||||
? `{bold|${formatDateVeryShort(date, locale, config)}}`
|
||||
: formatDateVeryShort(date, locale, config);
|
||||
}
|
||||
if (dayDifference > 7) {
|
||||
const label = formatDateVeryShort(date, locale, config);
|
||||
return date.getDate() === 1 ? `{bold|${label}}` : label;
|
||||
}
|
||||
if (dayDifference > 2) {
|
||||
return formatDateWeekdayShort(date, locale, config);
|
||||
}
|
||||
if (minutesDifference && minutesDifference < 5) {
|
||||
return formatTimeWithSeconds(date, locale, config);
|
||||
}
|
||||
if (
|
||||
date.getHours() === 0 &&
|
||||
date.getMinutes() === 0 &&
|
||||
date.getSeconds() === 0
|
||||
) {
|
||||
return (value: number | Date) => {
|
||||
const date = new Date(value);
|
||||
if (dayDifference > 88) {
|
||||
return date.getMonth() === 0
|
||||
? `{bold|${formatDateMonthYear(date, locale, config)}}`
|
||||
: formatDateMonth(date, locale, config);
|
||||
}
|
||||
if (dayDifference > 35) {
|
||||
return date.getDate() === 1
|
||||
? `{bold|${formatDateVeryShort(date, locale, config)}}`
|
||||
: formatDateVeryShort(date, locale, config);
|
||||
}
|
||||
if (dayDifference > 7) {
|
||||
const label = formatDateVeryShort(date, locale, config);
|
||||
return date.getDate() === 1 ? `{bold|${label}}` : label;
|
||||
}
|
||||
if (dayDifference > 2) {
|
||||
return formatDateWeekdayShort(date, locale, config);
|
||||
}
|
||||
// show only date for the beginning of the day
|
||||
return `{bold|${formatDateVeryShort(date, locale, config)}}`;
|
||||
}
|
||||
return formatTime(date, locale, config);
|
||||
if (
|
||||
date.getHours() === 0 &&
|
||||
date.getMinutes() === 0 &&
|
||||
date.getSeconds() === 0
|
||||
) {
|
||||
return `{bold|${formatDateVeryShort(date, locale, config)}}`;
|
||||
}
|
||||
return formatTime(date, locale, config);
|
||||
};
|
||||
}
|
||||
|
||||
export function getTimeAxisLabelConfig(
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
dayDifference?: number
|
||||
): XAXisOption["axisLabel"] {
|
||||
return {
|
||||
formatter: getLabelFormatter(locale, config, dayDifference),
|
||||
rich: {
|
||||
bold: {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
hideOverlap: true,
|
||||
};
|
||||
}
|
||||
|
@@ -1,37 +1,27 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { mdiChevronDown, mdiChevronUp, mdiRestart } from "@mdi/js";
|
||||
import { differenceInMinutes } from "date-fns";
|
||||
import type { DataZoomComponentOption } from "echarts/components";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, nothing, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { mdiRestart } from "@mdi/js";
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { DataZoomComponentOption } from "echarts/components";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import type {
|
||||
ECElementEvent,
|
||||
LegendComponentOption,
|
||||
XAXisOption,
|
||||
YAXisOption,
|
||||
} from "echarts/types/dist/shared";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { getAllGraphColors } from "../../common/color/colors";
|
||||
import { consume } from "@lit-labs/context";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import { themesContext } from "../../data/context";
|
||||
import type { Themes } from "../../data/ws-themes";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
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";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import type { Themes } from "../../data/ws-themes";
|
||||
import { themesContext } from "../../data/context";
|
||||
|
||||
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 {
|
||||
@@ -45,10 +35,8 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
@property({ type: String }) public height?: string;
|
||||
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@property({ attribute: false }) public extraComponents?: any[];
|
||||
@property({ attribute: "external-hidden", type: Boolean })
|
||||
public externalHidden = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: themesContext, subscribe: true })
|
||||
@@ -56,18 +44,10 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
@state() private _isZoomed = false;
|
||||
|
||||
@state() private _zoomRatio = 1;
|
||||
|
||||
@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(),
|
||||
@@ -79,16 +59,12 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _listeners: (() => void)[] = [];
|
||||
|
||||
private _originalZrFlush?: () => void;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
}
|
||||
this.chart?.dispose();
|
||||
this.chart = undefined;
|
||||
this._originalZrFlush = undefined;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -99,26 +75,20 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
this._listeners.push(
|
||||
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
||||
if (this._reducedMotion !== matches) {
|
||||
this._reducedMotion = matches;
|
||||
this._setChartOptions({ animation: !this._reducedMotion });
|
||||
}
|
||||
this._reducedMotion = matches;
|
||||
this.chart?.setOption({ animation: !this._reducedMotion });
|
||||
})
|
||||
);
|
||||
|
||||
// Add keyboard event listeners
|
||||
const handleKeyDown = (ev: KeyboardEvent) => {
|
||||
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||
if ((isMac && ev.metaKey) || (!isMac && ev.ctrlKey)) {
|
||||
this._modifierPressed = true;
|
||||
if (!this.options?.dataZoom) {
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
}
|
||||
// drag to zoom
|
||||
this.chart?.dispatchAction({
|
||||
type: "takeGlobalCursor",
|
||||
key: "dataZoomSelect",
|
||||
dataZoomSelectActive: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,13 +96,10 @@ export class HaChartBase extends LitElement {
|
||||
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||
this._modifierPressed = false;
|
||||
if (!this.options?.dataZoom) {
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
}
|
||||
this.chart?.dispatchAction({
|
||||
type: "takeGlobalCursor",
|
||||
key: "dataZoomSelect",
|
||||
dataZoomSelectActive: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -149,42 +116,49 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
if (!this.chart) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated || !this.chart) {
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("_themes")) {
|
||||
this._setupChart();
|
||||
return;
|
||||
}
|
||||
let chartOptions: ECOption = {};
|
||||
if (changedProps.has("data") || changedProps.has("_hiddenDatasets")) {
|
||||
chartOptions.series = this._getSeries();
|
||||
if (changedProps.has("data")) {
|
||||
this.chart.setOption(
|
||||
{ series: this.data },
|
||||
{ lazyUpdate: true, replaceMerge: ["series"] }
|
||||
);
|
||||
}
|
||||
if (changedProps.has("options")) {
|
||||
chartOptions = { ...chartOptions, ...this._createOptions() };
|
||||
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
|
||||
chartOptions.dataZoom = this._getDataZoomConfig();
|
||||
}
|
||||
if (Object.keys(chartOptions).length > 0) {
|
||||
this._setChartOptions(chartOptions);
|
||||
if (changedProps.has("options") || changedProps.has("_isZoomed")) {
|
||||
this.chart.setOption(this._createOptions(), {
|
||||
lazyUpdate: true,
|
||||
// if we replace the whole object, it will reset the dataZoom
|
||||
replaceMerge: [
|
||||
"xAxis",
|
||||
"yAxis",
|
||||
"dataZoom",
|
||||
"dataset",
|
||||
"tooltip",
|
||||
"legend",
|
||||
"grid",
|
||||
"visualMap",
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
class="container ${classMap({ "has-height": !!this.height })}"
|
||||
style=${styleMap({ height: this.height })}
|
||||
class="chart-container"
|
||||
style=${styleMap({
|
||||
height: this.height ?? `${this._getDefaultHeight()}px`,
|
||||
})}
|
||||
@wheel=${this._handleWheel}
|
||||
>
|
||||
<div
|
||||
class="chart-container"
|
||||
style=${styleMap({
|
||||
height: this.height ? undefined : `${this._getDefaultHeight()}px`,
|
||||
})}
|
||||
>
|
||||
<div class="chart"></div>
|
||||
</div>
|
||||
${this._renderLegend()}
|
||||
<div class="chart"></div>
|
||||
${this._isZoomed
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
@@ -199,82 +173,6 @@ 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,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this._minutesDifference * this._zoomRatio
|
||||
);
|
||||
|
||||
private async _setupChart() {
|
||||
if (this._loading) return;
|
||||
const container = this.renderRoot.querySelector(".chart") as HTMLDivElement;
|
||||
@@ -285,54 +183,45 @@ 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 = echarts.init(
|
||||
container,
|
||||
this._themes.darkMode ? "dark" : "light"
|
||||
);
|
||||
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;
|
||||
this._zoomRatio = (end - start) / 100;
|
||||
});
|
||||
this.chart.on("click", (e: ECElementEvent) => {
|
||||
fireEvent(this, "chart-click", e);
|
||||
});
|
||||
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.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.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
|
||||
| undefined;
|
||||
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
|
||||
| YAXisOption
|
||||
| undefined;
|
||||
if (xAxis?.type === "value" && yAxis?.type === "category") {
|
||||
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") {
|
||||
// vertical data zoom doesn't work well in this case and horizontal is pointless
|
||||
return undefined;
|
||||
}
|
||||
@@ -341,66 +230,31 @@ export class HaChartBase extends LitElement {
|
||||
type: "inside",
|
||||
orient: "horizontal",
|
||||
filterMode: "none",
|
||||
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
moveOnMouseMove: this._isZoomed,
|
||||
preventDefaultMouseMove: this._isZoomed,
|
||||
zoomLock: !this._isTouchDevice && !this._modifierPressed,
|
||||
};
|
||||
}
|
||||
|
||||
private _createOptions(): ECOption {
|
||||
let xAxis = this.options?.xAxis;
|
||||
if (xAxis) {
|
||||
xAxis = Array.isArray(xAxis) ? xAxis : [xAxis];
|
||||
xAxis = xAxis.map((axis: XAXisOption) => {
|
||||
if (axis.type !== "time" || axis.show === false) {
|
||||
return axis;
|
||||
}
|
||||
if (axis.max && axis.min) {
|
||||
this._minutesDifference = differenceInMinutes(
|
||||
axis.max as Date,
|
||||
axis.min as Date
|
||||
);
|
||||
}
|
||||
const dayDifference = this._minutesDifference / 60 / 24;
|
||||
let minInterval: number | undefined;
|
||||
if (dayDifference) {
|
||||
minInterval =
|
||||
dayDifference >= 89 // quarter
|
||||
? 28 * 3600 * 24 * 1000
|
||||
: dayDifference > 2
|
||||
? 3600 * 24 * 1000
|
||||
: undefined;
|
||||
}
|
||||
return {
|
||||
axisLine: { show: false },
|
||||
splitLine: { show: true },
|
||||
...axis,
|
||||
axisLabel: {
|
||||
formatter: this._formatTimeLabel,
|
||||
rich: { bold: { fontWeight: "bold" } },
|
||||
hideOverlap: true,
|
||||
...axis.axisLabel,
|
||||
},
|
||||
minInterval,
|
||||
} as XAXisOption;
|
||||
});
|
||||
}
|
||||
const darkMode = this._themes.darkMode ?? false;
|
||||
|
||||
const options = {
|
||||
backgroundColor: "transparent",
|
||||
animation: !this._reducedMotion,
|
||||
darkMode: this._themes.darkMode ?? false,
|
||||
aria: { show: true },
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
toolbox: {
|
||||
top: Infinity,
|
||||
left: Infinity,
|
||||
feature: {
|
||||
dataZoom: { show: true, yAxisIndex: false, filterMode: "none" },
|
||||
},
|
||||
iconStyle: { opacity: 0 },
|
||||
darkMode,
|
||||
aria: {
|
||||
show: true,
|
||||
},
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
...this.options,
|
||||
legend: { show: false },
|
||||
xAxis,
|
||||
legend: this.options?.legend
|
||||
? {
|
||||
// we should create our own theme but this is a quick fix for now
|
||||
inactiveColor: darkMode ? "#444" : "#ccc",
|
||||
...this.options.legend,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const isMobile = window.matchMedia(
|
||||
@@ -414,267 +268,48 @@ export class HaChartBase extends LitElement {
|
||||
tooltips.forEach((tooltip) => {
|
||||
tooltip.confine = true;
|
||||
tooltip.appendTo = undefined;
|
||||
tooltip.triggerOn = "click";
|
||||
});
|
||||
options.tooltip = tooltips;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private _createTheme() {
|
||||
const style = getComputedStyle(this);
|
||||
return {
|
||||
color: getAllGraphColors(style),
|
||||
backgroundColor: "transparent",
|
||||
textStyle: {
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
fontFamily: "Roboto, Noto, sans-serif",
|
||||
},
|
||||
title: {
|
||||
textStyle: { color: style.getPropertyValue("--primary-text-color") },
|
||||
subtextStyle: {
|
||||
color: style.getPropertyValue("--secondary-text-color"),
|
||||
},
|
||||
},
|
||||
line: {
|
||||
lineStyle: { width: 1.5 },
|
||||
symbolSize: 1,
|
||||
symbol: "circle",
|
||||
smooth: false,
|
||||
},
|
||||
bar: { itemStyle: { barBorderWidth: 1.5 } },
|
||||
categoryAxis: {
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
show: true,
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
splitArea: {
|
||||
show: false,
|
||||
areaStyle: {
|
||||
color: [
|
||||
style.getPropertyValue("--divider-color") + "3F",
|
||||
style.getPropertyValue("--divider-color") + "7F",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
valueAxis: {
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
splitArea: {
|
||||
show: false,
|
||||
areaStyle: {
|
||||
color: [
|
||||
style.getPropertyValue("--divider-color") + "3F",
|
||||
style.getPropertyValue("--divider-color") + "7F",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
logAxis: {
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
splitArea: {
|
||||
show: false,
|
||||
areaStyle: {
|
||||
color: [
|
||||
style.getPropertyValue("--divider-color") + "3F",
|
||||
style.getPropertyValue("--divider-color") + "7F",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
timeAxis: {
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
splitArea: {
|
||||
show: false,
|
||||
areaStyle: {
|
||||
color: [
|
||||
style.getPropertyValue("--divider-color") + "3F",
|
||||
style.getPropertyValue("--divider-color") + "7F",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
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"),
|
||||
pageTextStyle: {
|
||||
color: style.getPropertyValue("--secondary-text-color"),
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: style.getPropertyValue("--card-background-color"),
|
||||
borderColor: style.getPropertyValue("--divider-color"),
|
||||
textStyle: {
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
fontSize: 12,
|
||||
},
|
||||
axisPointer: {
|
||||
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);
|
||||
return Math.max(this.clientWidth / 2, 400);
|
||||
}
|
||||
|
||||
private _setChartOptions(options: ECOption) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (!this._originalZrFlush) {
|
||||
const dataSize = ensureArray(this.data).reduce(
|
||||
(acc, series) => acc + ((series.data as any[]) || []).length,
|
||||
0
|
||||
);
|
||||
if (dataSize > 10000) {
|
||||
// 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;
|
||||
zr.flush = () => {
|
||||
setTimeout(() => {
|
||||
this._originalZrFlush?.call(zr);
|
||||
}, 5);
|
||||
};
|
||||
}
|
||||
}
|
||||
const replaceMerge = options.series ? ["series"] : [];
|
||||
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;
|
||||
private _handleWheel(e: WheelEvent) {
|
||||
// if the window is not focused, we don't receive the keydown events but scroll still works
|
||||
if (!this.options?.dataZoom) {
|
||||
const modifierPressed = (isMac && e.metaKey) || (!isMac && e.ctrlKey);
|
||||
if (modifierPressed) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (modifierPressed !== this._modifierPressed) {
|
||||
this._modifierPressed = modifierPressed;
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
.container.has-height {
|
||||
max-height: var(--chart-max-height, 350px);
|
||||
}
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
max-height: var(--chart-max-height, 350px);
|
||||
}
|
||||
.has-height .chart-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
max-height: var(--chart-max-height, 400px);
|
||||
}
|
||||
.chart {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.zoom-reset {
|
||||
position: absolute;
|
||||
@@ -686,67 +321,6 @@ export class HaChartBase extends LitElement {
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
}
|
||||
.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,17 +1,9 @@
|
||||
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 { customElement, property } from "lit/decorators";
|
||||
import { LitElement, html, css, svg, nothing } from "lit";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import memoizeOne from "memoize-one";
|
||||
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;
|
||||
@@ -33,14 +25,34 @@ export interface SankeyChartData {
|
||||
links: Link[];
|
||||
}
|
||||
|
||||
type ProcessedLink = Link & {
|
||||
value: number;
|
||||
type ProcessedNode = Node & {
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
};
|
||||
|
||||
const OVERFLOW_MARGIN = 5;
|
||||
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 FONT_SIZE = 12;
|
||||
const NODE_GAP = 8;
|
||||
const LABEL_DISTANCE = 5;
|
||||
const MIN_DISTANCE = FONT_SIZE / 2;
|
||||
|
||||
@customElement("ha-sankey-chart")
|
||||
export class HaSankeyChart extends LitElement {
|
||||
@@ -53,144 +65,141 @@ export class HaSankeyChart extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public vertical = false;
|
||||
|
||||
@property({ type: String, attribute: false }) public valueFormatter?: (
|
||||
value: number
|
||||
) => string;
|
||||
@property({ attribute: false }) public loadingText?: string;
|
||||
|
||||
public chart?: EChartsType;
|
||||
private _statePerPixel = 0;
|
||||
|
||||
@state() private _sizeController = new ResizeController(this, {
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect,
|
||||
});
|
||||
|
||||
render() {
|
||||
const options = {
|
||||
grid: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: this._renderTooltip,
|
||||
appendTo: document.body,
|
||||
},
|
||||
} as ECOption;
|
||||
|
||||
return html`<ha-chart-base
|
||||
.data=${this._createData(this.data, this._sizeController.value?.width)}
|
||||
.options=${options}
|
||||
height="100%"
|
||||
.extraComponents=${[SankeyChart]}
|
||||
></ha-chart-base>`;
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
willUpdate() {
|
||||
this._statePerPixel = 0;
|
||||
}
|
||||
|
||||
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;
|
||||
render() {
|
||||
if (!this._sizeController.value) {
|
||||
return this.loadingText ?? nothing;
|
||||
}
|
||||
|
||||
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 { 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
|
||||
);
|
||||
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",
|
||||
};
|
||||
}
|
||||
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}
|
||||
`;
|
||||
}
|
||||
|
||||
// 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 _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 _processLinks(nodes: Node[], rawLinks: Link[]) {
|
||||
private _processLinks(nodes: Node[], indexes: number[], 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);
|
||||
@@ -213,25 +222,307 @@ 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;
|
||||
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);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
flex: 1;
|
||||
background: var(--ha-card-background, var(--card-background-color));
|
||||
background: var(--ha-card-background, var(--card-background-color, #000));
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
ha-chart-base {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import { property, state } from "lit/decorators";
|
||||
import type { VisualMapComponentOption } from "echarts/components";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
import type { YAXisOption } from "echarts/types/dist/shared";
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
@@ -17,10 +18,10 @@ import {
|
||||
getNumberFormatOptions,
|
||||
formatNumber,
|
||||
} from "../../common/number/format_number";
|
||||
import { getTimeAxisLabelConfig } from "./axis-label";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const parsed = parseFloat(value);
|
||||
@@ -63,9 +64,6 @@ 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[] = [];
|
||||
@@ -74,12 +72,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@state() private _chartOptions?: ECOption;
|
||||
|
||||
private _hiddenStats = new Set<string>();
|
||||
|
||||
@state() private _yWidth = 25;
|
||||
|
||||
@state() private _visualMap?: VisualMapComponentOption[];
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
protected render() {
|
||||
@@ -90,104 +84,49 @@ export class StateHistoryChartLine extends LitElement {
|
||||
.options=${this._chartOptions}
|
||||
.height=${this.height}
|
||||
style=${styleMap({ height: this.height })}
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
.expandLegend=${this.expandLegend}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTooltip = (params: any) => {
|
||||
const time = params[0].axisValue;
|
||||
const title =
|
||||
formatDateTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) + "<br>";
|
||||
const datapoints: Record<string, any>[] = [];
|
||||
this._chartData.forEach((dataset, index) => {
|
||||
if (
|
||||
dataset.tooltip?.show === false ||
|
||||
this._hiddenStats.has(dataset.name as string)
|
||||
)
|
||||
return;
|
||||
const param = params.find(
|
||||
(p: Record<string, any>) => p.seriesIndex === index
|
||||
);
|
||||
if (param) {
|
||||
datapoints.push(param);
|
||||
return;
|
||||
}
|
||||
// If the datapoint is not found, we need to find the last datapoint before the current time
|
||||
let lastData: any;
|
||||
const data = dataset.data || [];
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
const point = data[i];
|
||||
if (point && point[0] <= time && point[1]) {
|
||||
lastData = point;
|
||||
break;
|
||||
private _renderTooltip(params) {
|
||||
return params
|
||||
.map((param, index: number) => {
|
||||
let value = `${formatNumber(
|
||||
param.value[1] as number,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(
|
||||
undefined,
|
||||
this.hass.entities[this._entityIds[param.seriesIndex]]
|
||||
)
|
||||
)} ${this.unit}`;
|
||||
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (data.statistics && data.statistics.length > 0) {
|
||||
value += "<br> ";
|
||||
const source =
|
||||
data.states.length === 0 ||
|
||||
param.value[0] < data.states[0].last_changed
|
||||
? `${this.hass.localize(
|
||||
"ui.components.history_charts.source_stats"
|
||||
)}`
|
||||
: `${this.hass.localize(
|
||||
"ui.components.history_charts.source_history"
|
||||
)}`;
|
||||
value += source;
|
||||
}
|
||||
}
|
||||
if (!lastData) return;
|
||||
datapoints.push({
|
||||
seriesName: dataset.name,
|
||||
seriesIndex: index,
|
||||
value: lastData,
|
||||
// HTML copied from echarts. May change based on options
|
||||
marker: `<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${dataset.color};"></span>`,
|
||||
});
|
||||
});
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
|
||||
return (
|
||||
title +
|
||||
datapoints
|
||||
.map((param) => {
|
||||
const entityId = this._entityIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const entry = this.hass.entities[entityId];
|
||||
const stateValue = String(param.value[1]);
|
||||
let value = stateObj
|
||||
? this.hass.formatEntityState(stateObj, stateValue)
|
||||
: `${formatNumber(
|
||||
stateValue,
|
||||
const time =
|
||||
index === 0
|
||||
? formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(undefined, entry)
|
||||
)}${unit}`;
|
||||
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (data.statistics && data.statistics.length > 0) {
|
||||
value += "<br> ";
|
||||
const source =
|
||||
data.states.length === 0 ||
|
||||
param.value[0] < data.states[0].last_changed
|
||||
? `${this.hass.localize(
|
||||
"ui.components.history_charts.source_stats"
|
||||
)}`
|
||||
: `${this.hass.localize(
|
||||
"ui.components.history_charts.source_history"
|
||||
)}`;
|
||||
value += source;
|
||||
}
|
||||
|
||||
if (param.seriesName) {
|
||||
return `${param.marker} ${param.seriesName}: ${value}`;
|
||||
}
|
||||
return `${param.marker} ${value}`;
|
||||
})
|
||||
.join("<br>")
|
||||
);
|
||||
};
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
this._hiddenStats.add(ev.detail.name);
|
||||
}
|
||||
|
||||
private _datasetUnhidden(ev: CustomEvent) {
|
||||
this._hiddenStats.delete(ev.detail.name);
|
||||
this.hass.config
|
||||
) + "<br>"
|
||||
: "";
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}
|
||||
`;
|
||||
})
|
||||
.join("<br>");
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
@@ -213,67 +152,58 @@ export class StateHistoryChartLine extends LitElement {
|
||||
changedProps.has("minYAxis") ||
|
||||
changedProps.has("maxYAxis") ||
|
||||
changedProps.has("fitYData") ||
|
||||
changedProps.has("_chartData") ||
|
||||
changedProps.has("paddingYAxis") ||
|
||||
changedProps.has("_visualMap") ||
|
||||
changedProps.has("_yWidth")
|
||||
) {
|
||||
const dayDifference = differenceInDays(this.endTime, this.startTime);
|
||||
const rtl = computeRTL(this.hass);
|
||||
let minYAxis: number | ((values: { min: number }) => number) | undefined =
|
||||
this.minYAxis;
|
||||
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
|
||||
this.maxYAxis;
|
||||
if (typeof minYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
|
||||
}
|
||||
if (typeof maxYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
|
||||
}
|
||||
const splitLineStyle = this.hass.themes?.darkMode
|
||||
? { opacity: 0.15 }
|
||||
: {};
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: this.startTime,
|
||||
max: this.endTime,
|
||||
axisLabel: getTimeAxisLabelConfig(
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
dayDifference
|
||||
),
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: splitLineStyle,
|
||||
},
|
||||
minInterval:
|
||||
dayDifference >= 89 // quarter
|
||||
? 28 * 3600 * 24 * 1000
|
||||
: dayDifference > 2
|
||||
? 3600 * 24 * 1000
|
||||
: undefined,
|
||||
},
|
||||
yAxis: {
|
||||
type: this.logarithmicScale ? "log" : "value",
|
||||
name: this.unit,
|
||||
min: this._clampYAxis(minYAxis),
|
||||
max: this._clampYAxis(maxYAxis),
|
||||
min: this.fitYData ? this.minYAxis : undefined,
|
||||
max: this.fitYData ? this.maxYAxis : undefined,
|
||||
position: rtl ? "right" : "left",
|
||||
scale: true,
|
||||
nameGap: 2,
|
||||
nameTextStyle: {
|
||||
align: "left",
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: splitLineStyle,
|
||||
},
|
||||
axisLabel: {
|
||||
margin: 5,
|
||||
formatter: (value: number) => {
|
||||
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 label = formatNumber(value, this.hass.locale);
|
||||
const width = measureTextWidth(label, 12) + 5;
|
||||
if (width > this._yWidth) {
|
||||
this._yWidth = width;
|
||||
@@ -288,18 +218,46 @@ export class StateHistoryChartLine extends LitElement {
|
||||
} as YAXisOption,
|
||||
legend: {
|
||||
show: this.showNames,
|
||||
icon: "circle",
|
||||
padding: [20, 0],
|
||||
},
|
||||
grid: {
|
||||
top: 15,
|
||||
...(this.showNames ? {} : { top: 30 }), // undefined is the same as 0
|
||||
left: rtl ? 1 : Math.max(this.paddingYAxis, this._yWidth),
|
||||
right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1,
|
||||
bottom: 20,
|
||||
bottom: 30,
|
||||
},
|
||||
visualMap: this._visualMap,
|
||||
visualMap: this._chartData
|
||||
.map((_, seriesIndex) => {
|
||||
const dataIndex = this._datasetToDataIndex[seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (!data.statistics || data.statistics.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// render stat data with a slightly transparent line
|
||||
const firstStateTS =
|
||||
data.states[0]?.last_changed ?? this.endTime.getTime();
|
||||
return {
|
||||
show: false,
|
||||
seriesIndex,
|
||||
dimension: 0,
|
||||
pieces: [
|
||||
{
|
||||
max: firstStateTS - 0.01,
|
||||
colorAlpha: 0.5,
|
||||
},
|
||||
{
|
||||
min: firstStateTS,
|
||||
colorAlpha: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as VisualMapComponentOption[],
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
appendTo: document.body,
|
||||
formatter: this._renderTooltip,
|
||||
formatter: this._renderTooltip.bind(this),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -349,28 +307,21 @@ export class StateHistoryChartLine extends LitElement {
|
||||
prevValues = datavalues;
|
||||
};
|
||||
|
||||
const addDataSet = (
|
||||
id: string,
|
||||
nameY: string,
|
||||
color?: string,
|
||||
fill = false
|
||||
) => {
|
||||
const addDataSet = (nameY: string, color?: string, fill = false) => {
|
||||
if (!color) {
|
||||
color = getGraphColorByIndex(colorIndex, computedStyles);
|
||||
colorIndex++;
|
||||
}
|
||||
data.push({
|
||||
id,
|
||||
id: nameY,
|
||||
data: [],
|
||||
type: "line",
|
||||
cursor: "default",
|
||||
name: nameY,
|
||||
color,
|
||||
symbol: "circle",
|
||||
symbolSize: 1,
|
||||
step: "end",
|
||||
sampling: "minmax",
|
||||
animationDurationUpdate: 0,
|
||||
symbolSize: 1,
|
||||
lineStyle: {
|
||||
width: fill ? 0 : 1.5,
|
||||
},
|
||||
@@ -424,23 +375,13 @@ export class StateHistoryChartLine extends LitElement {
|
||||
entityState.attributes.target_temp_low
|
||||
);
|
||||
addDataSet(
|
||||
states.entity_id + "-current_temperature",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.current_temperature", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.current_temperature.name"
|
||||
)
|
||||
`${this.hass.localize("ui.card.climate.current_temperature", {
|
||||
name: name,
|
||||
})}`
|
||||
);
|
||||
if (hasHeat) {
|
||||
addDataSet(
|
||||
states.entity_id + "-heating",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.heating", { name: name })
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.hvac_action.state.heating"
|
||||
),
|
||||
`${this.hass.localize("ui.card.climate.heating", { name: name })}`,
|
||||
computedStyles.getPropertyValue("--state-climate-heat-color"),
|
||||
true
|
||||
);
|
||||
@@ -449,12 +390,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
if (hasCool) {
|
||||
addDataSet(
|
||||
states.entity_id + "-cooling",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.cooling", { name: name })
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.hvac_action.state.cooling"
|
||||
),
|
||||
`${this.hass.localize("ui.card.climate.cooling", { name: name })}`,
|
||||
computedStyles.getPropertyValue("--state-climate-cool-color"),
|
||||
true
|
||||
);
|
||||
@@ -464,40 +400,22 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
if (hasTargetRange) {
|
||||
addDataSet(
|
||||
states.entity_id + "-target_temperature_mode",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.high"),
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.target_temp_high.name"
|
||||
)
|
||||
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.high"),
|
||||
})}`
|
||||
);
|
||||
addDataSet(
|
||||
states.entity_id + "-target_temperature_mode_low",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.low"),
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.target_temp_low.name"
|
||||
)
|
||||
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.low"),
|
||||
})}`
|
||||
);
|
||||
} else {
|
||||
addDataSet(
|
||||
states.entity_id + "-target_temperature",
|
||||
this.showNames
|
||||
? this.hass.localize(
|
||||
"ui.card.climate.target_temperature_entity",
|
||||
{
|
||||
name: name,
|
||||
}
|
||||
)
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.temperature.name"
|
||||
)
|
||||
`${this.hass.localize("ui.card.climate.target_temperature_entity", {
|
||||
name: name,
|
||||
})}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -550,29 +468,19 @@ export class StateHistoryChartLine extends LitElement {
|
||||
);
|
||||
|
||||
addDataSet(
|
||||
states.entity_id + "-target_humidity",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.humidifier.target_humidity_entity", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state_attributes.humidity.name"
|
||||
)
|
||||
`${this.hass.localize("ui.card.humidifier.target_humidity_entity", {
|
||||
name: name,
|
||||
})}`
|
||||
);
|
||||
|
||||
if (hasCurrent) {
|
||||
addDataSet(
|
||||
states.entity_id + "-current_humidity",
|
||||
this.showNames
|
||||
? this.hass.localize(
|
||||
"ui.card.humidifier.current_humidity_entity",
|
||||
{
|
||||
name: name,
|
||||
}
|
||||
)
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state_attributes.current_humidity.name"
|
||||
)
|
||||
`${this.hass.localize(
|
||||
"ui.card.humidifier.current_humidity_entity",
|
||||
{
|
||||
name: name,
|
||||
}
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -580,40 +488,25 @@ export class StateHistoryChartLine extends LitElement {
|
||||
// If action attribute is not available, we shade the area when the device is on
|
||||
if (hasHumidifying) {
|
||||
addDataSet(
|
||||
states.entity_id + "-humidifying",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.humidifier.humidifying", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state_attributes.action.state.humidifying"
|
||||
),
|
||||
`${this.hass.localize("ui.card.humidifier.humidifying", {
|
||||
name: name,
|
||||
})}`,
|
||||
computedStyles.getPropertyValue("--state-humidifier-on-color"),
|
||||
true
|
||||
);
|
||||
} else if (hasDrying) {
|
||||
addDataSet(
|
||||
states.entity_id + "-drying",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.humidifier.drying", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state_attributes.action.state.drying"
|
||||
),
|
||||
`${this.hass.localize("ui.card.humidifier.drying", {
|
||||
name: name,
|
||||
})}`,
|
||||
computedStyles.getPropertyValue("--state-humidifier-on-color"),
|
||||
true
|
||||
);
|
||||
} else {
|
||||
addDataSet(
|
||||
states.entity_id + "-on",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.humidifier.on_entity", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state.on"
|
||||
),
|
||||
`${this.hass.localize("ui.card.humidifier.on_entity", {
|
||||
name: name,
|
||||
})}`,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
@@ -646,7 +539,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
pushData(new Date(entityState.last_changed), series);
|
||||
});
|
||||
} else {
|
||||
addDataSet(states.entity_id, name);
|
||||
addDataSet(name);
|
||||
|
||||
let lastValue: number;
|
||||
let lastDate: Date;
|
||||
@@ -715,46 +608,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
this._chartData = datasets;
|
||||
this._entityIds = entityIds;
|
||||
this._datasetToDataIndex = datasetToDataIndex;
|
||||
const visualMap: VisualMapComponentOption[] = [];
|
||||
this._chartData.forEach((_, seriesIndex) => {
|
||||
const dataIndex = this._datasetToDataIndex[seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (!data.statistics || data.statistics.length === 0) {
|
||||
return;
|
||||
}
|
||||
// render stat data with a slightly transparent line
|
||||
const firstStateTS =
|
||||
data.states[0]?.last_changed ?? this.endTime.getTime();
|
||||
visualMap.push({
|
||||
show: false,
|
||||
seriesIndex,
|
||||
dimension: 0,
|
||||
pieces: [
|
||||
{
|
||||
max: firstStateTS - 0.01,
|
||||
colorAlpha: 0.5,
|
||||
},
|
||||
{
|
||||
min: firstStateTS,
|
||||
colorAlpha: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
this._visualMap = visualMap.length > 0 ? visualMap : undefined;
|
||||
}
|
||||
|
||||
private _clampYAxis(value?: number | ((values: any) => number)) {
|
||||
if (this.logarithmicScale) {
|
||||
// log(0) is -Infinity, so we need to set a minimum value
|
||||
if (typeof value === "number") {
|
||||
return Math.max(value, 0.1);
|
||||
}
|
||||
if (typeof value === "function") {
|
||||
return (values: any) => Math.max(value(values), 0.1);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
customElements.define("state-history-chart-line", StateHistoryChartLine);
|
||||
|
@@ -8,6 +8,7 @@ import type {
|
||||
TooltipFormatterCallback,
|
||||
TooltipPositionCallbackParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
@@ -21,6 +22,7 @@ import { luminosity } from "../../common/color/rgb";
|
||||
import { hex2rgb } from "../../common/color/convert-color";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { getTimeAxisLabelConfig } from "./axis-label";
|
||||
|
||||
@customElement("state-history-chart-timeline")
|
||||
export class StateHistoryChartTimeline extends LitElement {
|
||||
@@ -65,7 +67,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.options=${this._chartOptions}
|
||||
.height=${`${this.data.length * 30 + 30}px`}
|
||||
.data=${this._chartData as ECOption["series"]}
|
||||
.data=${this._chartData}
|
||||
@chart-click=${this._handleChartClick}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
@@ -127,12 +129,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
private _renderTooltip: TooltipFormatterCallback<TooltipPositionCallbackParams> =
|
||||
(params: TooltipPositionCallbackParams) => {
|
||||
const { value, name, marker, seriesName } = Array.isArray(params)
|
||||
const { value, name, marker } = Array.isArray(params)
|
||||
? params[0]
|
||||
: params;
|
||||
const title = seriesName
|
||||
? `<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
|
||||
: "";
|
||||
const title = `<h4 style="text-align: center; margin: 0;">${value![0]}</h4>`;
|
||||
const durationInMs = value![2] - value![1];
|
||||
const formattedDuration = `${this.hass.localize(
|
||||
"ui.components.history_charts.duration"
|
||||
@@ -183,12 +183,13 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
private _createOptions() {
|
||||
const narrow = this.narrow;
|
||||
const showNames = this.chunked || this.showNames;
|
||||
const maxInternalLabelWidth = narrow ? 105 : 185;
|
||||
const maxInternalLabelWidth = narrow ? 70 : 165;
|
||||
const labelWidth = showNames
|
||||
? Math.max(this.paddingYAxis, this._yWidth)
|
||||
: 0;
|
||||
const labelMargin = 5;
|
||||
const rtl = computeRTL(this.hass);
|
||||
const dayDifference = differenceInDays(this.endTime, this.startTime);
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
@@ -196,10 +197,21 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
max: this.endTime,
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: getTimeAxisLabelConfig(
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
dayDifference
|
||||
),
|
||||
minInterval:
|
||||
dayDifference >= 89 // quarter
|
||||
? 28 * 3600 * 24 * 1000
|
||||
: dayDifference > 2
|
||||
? 3600 * 24 * 1000
|
||||
: undefined,
|
||||
},
|
||||
yAxis: {
|
||||
type: "category",
|
||||
@@ -214,18 +226,14 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
},
|
||||
axisLabel: {
|
||||
show: showNames,
|
||||
width: labelWidth,
|
||||
width: labelWidth - labelMargin,
|
||||
overflow: "truncate",
|
||||
margin: labelMargin,
|
||||
formatter: (id: string) => {
|
||||
const label = this._chartData.find((d) => d.id === id)
|
||||
?.name as string;
|
||||
const width = label
|
||||
? Math.min(
|
||||
measureTextWidth(label, 12) + labelMargin,
|
||||
maxInternalLabelWidth
|
||||
)
|
||||
: 0;
|
||||
formatter: (label: string) => {
|
||||
const width = Math.min(
|
||||
measureTextWidth(label, 12) + labelMargin,
|
||||
maxInternalLabelWidth
|
||||
);
|
||||
if (width > this._yWidth) {
|
||||
this._yWidth = width;
|
||||
fireEvent(this, "y-width-changed", {
|
||||
@@ -270,9 +278,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
let prevState: string | null = null;
|
||||
let locState: string | null = null;
|
||||
let prevLastChanged = startTime;
|
||||
const entityDisplay: string = this.showNames
|
||||
? names[stateInfo.entity_id] || stateInfo.name || stateInfo.entity_id
|
||||
: "";
|
||||
const entityDisplay: string =
|
||||
names[stateInfo.entity_id] || stateInfo.name;
|
||||
|
||||
const dataRow: unknown[] = [];
|
||||
stateInfo.data.forEach((entityState) => {
|
||||
@@ -300,7 +307,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
);
|
||||
dataRow.push({
|
||||
value: [
|
||||
stateInfo.entity_id,
|
||||
entityDisplay,
|
||||
prevLastChanged,
|
||||
newLastChanged,
|
||||
locState,
|
||||
@@ -326,7 +333,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
);
|
||||
dataRow.push({
|
||||
value: [
|
||||
stateInfo.entity_id,
|
||||
entityDisplay,
|
||||
prevLastChanged,
|
||||
endTime,
|
||||
locState,
|
||||
@@ -339,10 +346,9 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
});
|
||||
}
|
||||
datasets.push({
|
||||
id: stateInfo.entity_id,
|
||||
data: dataRow,
|
||||
name: entityDisplay,
|
||||
dimensions: ["id", "start", "end", "name", "color", "textColor"],
|
||||
dimensions: ["index", "start", "end", "name", "color", "textColor"],
|
||||
type: "custom",
|
||||
encode: {
|
||||
x: [1, 2],
|
||||
@@ -358,10 +364,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
private _handleChartClick(e: CustomEvent<ECElementEvent>): void {
|
||||
if (e.detail.targetType === "axisLabel") {
|
||||
const dataset = this._chartData[e.detail.dataIndex];
|
||||
const dataset = this.data[e.detail.dataIndex];
|
||||
if (dataset) {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: dataset.id as string,
|
||||
entityId: dataset.entity_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -71,9 +71,6 @@ 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;
|
||||
@@ -138,7 +135,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
if (!Array.isArray(item)) {
|
||||
return html`<div class="entry-container line">
|
||||
return html`<div class="entry-container">
|
||||
<state-history-chart-line
|
||||
.hass=${this.hass}
|
||||
.unit=${item.unit}
|
||||
@@ -157,11 +154,10 @@ 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> `;
|
||||
}
|
||||
return html`<div class="entry-container timeline">
|
||||
return html`<div class="entry-container">
|
||||
<state-history-chart-timeline
|
||||
.hass=${this.hass}
|
||||
.data=${item}
|
||||
@@ -303,12 +299,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
.entry-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entry-container.line {
|
||||
flex: 1;
|
||||
padding-top: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.entry-container:hover {
|
||||
@@ -322,10 +313,6 @@ export class StateHistoryCharts extends LitElement {
|
||||
padding-inline-end: 1px;
|
||||
}
|
||||
|
||||
.entry-container.timeline:first-child {
|
||||
margin-top: var(--timeline-top-margin);
|
||||
}
|
||||
|
||||
.entry-container:not(:first-child) {
|
||||
border-top: 2px solid var(--divider-color);
|
||||
margin-top: 16px;
|
||||
|
@@ -1,23 +1,15 @@
|
||||
import type {
|
||||
BarSeriesOption,
|
||||
LineSeriesOption,
|
||||
ZRColor,
|
||||
} from "echarts/types/dist/shared";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type {
|
||||
BarSeriesOption,
|
||||
LineSeriesOption,
|
||||
} from "echarts/types/dist/shared";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
} from "../../common/number/format_number";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type {
|
||||
Statistics,
|
||||
StatisticsMetaData,
|
||||
@@ -29,9 +21,16 @@ import {
|
||||
getStatisticMetadata,
|
||||
statisticsHaveType,
|
||||
} from "../../data/recorder";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
} from "../../common/number/format_number";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import { getTimeAxisLabelConfig } from "./axis-label";
|
||||
|
||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||
mean: "mean",
|
||||
@@ -57,8 +56,6 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@property() public unit?: string;
|
||||
|
||||
@property({ attribute: false }) public startTime?: Date;
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
@@ -91,9 +88,6 @@ 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[] = [];
|
||||
@@ -130,10 +124,7 @@ export class StatisticsChart extends LitElement {
|
||||
changedProps.has("fitYData") ||
|
||||
changedProps.has("logarithmicScale") ||
|
||||
changedProps.has("hideLegend") ||
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("_legendData") ||
|
||||
changedProps.has("_chartData")
|
||||
changedProps.has("_legendData")
|
||||
) {
|
||||
this._createOptions();
|
||||
}
|
||||
@@ -173,9 +164,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>
|
||||
`;
|
||||
}
|
||||
@@ -190,31 +181,18 @@ export class StatisticsChart extends LitElement {
|
||||
this.requestUpdate("_hiddenStats");
|
||||
}
|
||||
|
||||
private _renderTooltip = (params: any) => {
|
||||
const rendered: Record<string, boolean> = {};
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
return params
|
||||
private _renderTooltip = (params: any) =>
|
||||
params
|
||||
.map((param, index: number) => {
|
||||
if (rendered[param.seriesName]) return "";
|
||||
rendered[param.seriesName] = true;
|
||||
|
||||
const statisticId = this._statisticIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
const entry = this.hass.entities[statisticId];
|
||||
// max series can have 3 values, as the second value is the max-min to form a band
|
||||
const rawValue = String(param.value[2] ?? param.value[1]);
|
||||
|
||||
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
const value = `${formatNumber(
|
||||
rawValue,
|
||||
// max series can have 3 values, as the second value is the max-min to form a band
|
||||
(param.value[2] ?? param.value[1]) as number,
|
||||
this.hass.locale,
|
||||
options
|
||||
)}${unit}`;
|
||||
getNumberFormatOptions(
|
||||
undefined,
|
||||
this.hass.entities[this._statisticIds[param.seriesIndex]]
|
||||
)
|
||||
)} ${this.unit}`;
|
||||
|
||||
const time =
|
||||
index === 0
|
||||
@@ -224,70 +202,36 @@ export class StatisticsChart extends LitElement {
|
||||
this.hass.config
|
||||
) + "<br>"
|
||||
: "";
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}`;
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}
|
||||
`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("<br>");
|
||||
};
|
||||
|
||||
private _createOptions() {
|
||||
const splitLineStyle = this.hass.themes?.darkMode ? { opacity: 0.15 } : {};
|
||||
const dayDifference = this.daysToShow ?? 1;
|
||||
let minYAxis: number | ((values: { min: number }) => number) | undefined =
|
||||
this.minYAxis;
|
||||
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
|
||||
this.maxYAxis;
|
||||
if (typeof minYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
|
||||
}
|
||||
if (typeof maxYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
|
||||
}
|
||||
const endTime = this.endTime ?? new Date();
|
||||
let startTime = this.startTime;
|
||||
|
||||
if (!startTime) {
|
||||
// set start time to the earliest point in the chart data
|
||||
this._chartData.forEach((series) => {
|
||||
if (!Array.isArray(series.data) || !series.data[0]) return;
|
||||
const firstPoint = series.data[0] as any;
|
||||
const timestamp = Array.isArray(firstPoint)
|
||||
? firstPoint[0]
|
||||
: firstPoint.value?.[0];
|
||||
if (timestamp && (!startTime || new Date(timestamp) < startTime)) {
|
||||
startTime = new Date(timestamp);
|
||||
}
|
||||
});
|
||||
|
||||
if (!startTime) {
|
||||
// Calculate default start time based on dayDifference
|
||||
startTime = new Date(
|
||||
endTime.getTime() - dayDifference * 24 * 3600 * 1000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this._chartOptions = {
|
||||
xAxis: [
|
||||
{
|
||||
id: "xAxis",
|
||||
type: "time",
|
||||
min: startTime,
|
||||
max: this.endTime,
|
||||
},
|
||||
{
|
||||
id: "hiddenAxis",
|
||||
type: "time",
|
||||
xAxis: {
|
||||
type: "time",
|
||||
axisLabel: getTimeAxisLabelConfig(
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
dayDifference
|
||||
),
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
],
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: splitLineStyle,
|
||||
},
|
||||
minInterval:
|
||||
dayDifference >= 89 // quarter
|
||||
? 28 * 3600 * 24 * 1000
|
||||
: dayDifference > 2
|
||||
? 3600 * 24 * 1000
|
||||
: undefined,
|
||||
},
|
||||
yAxis: {
|
||||
type: this.logarithmicScale ? "log" : "value",
|
||||
name: this.unit,
|
||||
@@ -296,20 +240,24 @@ export class StatisticsChart extends LitElement {
|
||||
align: "left",
|
||||
},
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
scale: true,
|
||||
min: this._clampYAxis(minYAxis),
|
||||
max: this._clampYAxis(maxYAxis),
|
||||
// @ts-ignore
|
||||
scale: this.chartType !== "bar",
|
||||
min: this.fitYData ? undefined : this.minYAxis,
|
||||
max: this.fitYData ? undefined : this.maxYAxis,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: splitLineStyle,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: !this.hideLegend,
|
||||
icon: "circle",
|
||||
padding: [20, 0],
|
||||
data: this._legendData,
|
||||
},
|
||||
grid: {
|
||||
top: 15,
|
||||
left: 1,
|
||||
...(this.hideLegend ? { top: this.unit ? 30 : 5 } : {}), // undefined is the same as 0
|
||||
left: 20,
|
||||
right: 1,
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
@@ -348,11 +296,7 @@ export class StatisticsChart extends LitElement {
|
||||
let colorIndex = 0;
|
||||
const statisticsData = Object.entries(this.statisticsData);
|
||||
const totalDataSets: typeof this._chartData = [];
|
||||
const legendData: {
|
||||
name: string;
|
||||
color?: ZRColor;
|
||||
borderColor?: ZRColor;
|
||||
}[] = [];
|
||||
const legendData: { name: string; color: string }[] = [];
|
||||
const statisticIds: string[] = [];
|
||||
let endTime: Date;
|
||||
|
||||
@@ -403,7 +347,7 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
// The datasets for the current statistic
|
||||
const statDataSets: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
const statLegendData: typeof legendData = [];
|
||||
const statLegendData: { name: string; color: string }[] = [];
|
||||
|
||||
const pushData = (
|
||||
start: Date,
|
||||
@@ -425,12 +369,10 @@ export class StatisticsChart extends LitElement {
|
||||
) {
|
||||
// if the end of the previous data doesn't match the start of the current data,
|
||||
// we have to draw a gap so add a value at the end time, and then an empty value.
|
||||
d.data!.push(
|
||||
this._transformDataValue([prevEndTime, ...prevValues[i]!])
|
||||
);
|
||||
d.data!.push([prevEndTime, ...prevValues[i]!]);
|
||||
d.data!.push([prevEndTime, null]);
|
||||
}
|
||||
d.data!.push(this._transformDataValue([start, ...dataValues[i]!]));
|
||||
d.data!.push([start, ...dataValues[i]!]);
|
||||
});
|
||||
prevValues = dataValues;
|
||||
prevEndTime = end;
|
||||
@@ -469,15 +411,19 @@ 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;
|
||||
const backgroundColor = band ? color + "3F" : color + "7F";
|
||||
const series: LineSeriesOption | BarSeriesOption = {
|
||||
id: `${statistic_id}-${type}`,
|
||||
type: this.chartType,
|
||||
smooth: this.chartType === "line" ? 0.4 : false,
|
||||
smoothMonotone: "x",
|
||||
cursor: "default",
|
||||
data: [],
|
||||
name: name
|
||||
@@ -487,9 +433,8 @@ export class StatisticsChart extends LitElement {
|
||||
: this.hass.localize(
|
||||
`ui.components.statistics_charts.statistic_types.${type}`
|
||||
),
|
||||
symbol: "none",
|
||||
sampling: "minmax",
|
||||
animationDurationUpdate: 0,
|
||||
symbol: "circle",
|
||||
symbolSize: 0,
|
||||
lineStyle: {
|
||||
width: 1.5,
|
||||
},
|
||||
@@ -497,34 +442,27 @@ export class StatisticsChart extends LitElement {
|
||||
this.chartType === "bar"
|
||||
? {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderColor,
|
||||
borderColor:
|
||||
band && hasMean
|
||||
? color + (this.hideLegend ? "00" : "7F")
|
||||
: color,
|
||||
borderWidth: 1.5,
|
||||
}
|
||||
: undefined,
|
||||
color: this.chartType === "bar" ? backgroundColor : borderColor,
|
||||
color: band ? color + "3F" : color + "7F",
|
||||
};
|
||||
if (band && this.chartType === "line") {
|
||||
series.stack = `band-${statistic_id}`;
|
||||
series.stackStrategy = "all";
|
||||
(series as LineSeriesOption).symbol = "none";
|
||||
(series as LineSeriesOption).lineStyle = {
|
||||
opacity: 0,
|
||||
};
|
||||
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);
|
||||
}
|
||||
@@ -551,7 +489,7 @@ export class StatisticsChart extends LitElement {
|
||||
}
|
||||
} else if (type === "max" && this.chartType === "line") {
|
||||
const max = stat.max || 0;
|
||||
val.push(Math.abs(max - (stat.min || 0)));
|
||||
val.push(max - (stat.min || 0));
|
||||
val.push(max);
|
||||
} else {
|
||||
val.push(stat[type] ?? null);
|
||||
@@ -572,18 +510,14 @@ export class StatisticsChart extends LitElement {
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
legendData.forEach(({ name, color, borderColor }) => {
|
||||
legendData.forEach(({ name, color }) => {
|
||||
// Add an empty series for the legend
|
||||
totalDataSets.push({
|
||||
id: name + "-legend",
|
||||
name: name,
|
||||
color,
|
||||
itemStyle: {
|
||||
borderColor,
|
||||
},
|
||||
type: this.chartType,
|
||||
data: [],
|
||||
xAxisIndex: 1,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -595,26 +529,6 @@ export class StatisticsChart extends LitElement {
|
||||
this._statisticIds = statisticIds;
|
||||
}
|
||||
|
||||
private _transformDataValue(val: [Date, ...(number | null)[]]) {
|
||||
if (this.chartType === "bar" && val[1] && val[1] < 0) {
|
||||
return { value: val, itemStyle: { borderRadius: [0, 0, 4, 4] } };
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
private _clampYAxis(value?: number | ((values: any) => number)) {
|
||||
if (this.logarithmicScale) {
|
||||
// log(0) is -Infinity, so we need to set a minimum value
|
||||
if (typeof value === "number") {
|
||||
return Math.max(value, 0.1);
|
||||
}
|
||||
if (typeof value === "function") {
|
||||
return (values: any) => Math.max(value(values), 0.1);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
|
||||
@customElement("ha-data-table-icon")
|
||||
class HaDataTableIcon extends LitElement {
|
||||
@@ -10,14 +9,30 @@ class HaDataTableIcon extends LitElement {
|
||||
|
||||
@property() public path!: string;
|
||||
|
||||
@state() private _hovered = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-tooltip .content=${this.tooltip}>
|
||||
<ha-svg-icon .path=${this.path}></ha-svg-icon>
|
||||
</ha-tooltip>
|
||||
${this._hovered ? html`<div>${this.tooltip}</div>` : ""}
|
||||
<ha-svg-icon .path=${this.path}></ha-svg-icon>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -26,6 +41,20 @@ 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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -448,7 +448,6 @@ export class HaDataTable extends LitElement {
|
||||
)}
|
||||
@click=${this._handleHeaderClick}
|
||||
.columnId=${key}
|
||||
title=${ifDefined(column.title)}
|
||||
>
|
||||
${column.sortable
|
||||
? html`
|
||||
|
@@ -57,7 +57,7 @@ class HaEntityStatePicker extends LitElement {
|
||||
(this._comboBox as any).items = [
|
||||
...(this.extraOptions ?? []),
|
||||
...(this.entityId && stateObj
|
||||
? getStates(this.hass, stateObj, this.attribute).map((key) => ({
|
||||
? getStates(stateObj, this.attribute).map((key) => ({
|
||||
value: key,
|
||||
label: !this.attribute
|
||||
? this.hass.formatEntityState(stateObj, key)
|
||||
|
@@ -1,3 +1,4 @@
|
||||
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";
|
||||
@@ -5,7 +6,6 @@ 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-tooltip>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
<div slot="content">
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
<simple-tooltip animation-delay="0">
|
||||
<div>
|
||||
<div class="row">
|
||||
<span class="column-name">
|
||||
${this.hass.localize(
|
||||
@@ -68,7 +68,7 @@ class StateInfo extends LitElement {
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
</div>
|
||||
</ha-tooltip>
|
||||
</simple-tooltip>
|
||||
</div>`
|
||||
: html`<div class="extra-info"><slot></slot></div>`}
|
||||
</div>`;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -7,7 +8,6 @@ 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,21 +67,22 @@ export class HaAnalytics extends LitElement {
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
<ha-tooltip
|
||||
content=${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
placement="right"
|
||||
?disabled=${baseEnabled}
|
||||
<ha-switch
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
>
|
||||
<ha-switch
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-tooltip>
|
||||
</ha-switch>
|
||||
${!baseEnabled
|
||||
? html`
|
||||
<simple-tooltip animation-delay="0" position="right">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</simple-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
`
|
||||
|
@@ -295,12 +295,10 @@ export class HaAssistChat extends LitElement {
|
||||
this._addMessage(userMessage);
|
||||
this.requestUpdate("_audioRecorder");
|
||||
|
||||
let hassMessage = {
|
||||
const hassMessage: AssistMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
error: false,
|
||||
};
|
||||
let currentDeltaRole = "";
|
||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
@@ -330,43 +328,6 @@ export class HaAssistChat extends LitElement {
|
||||
this._addMessage(hassMessage);
|
||||
}
|
||||
|
||||
if (event.type === "intent-progress") {
|
||||
const delta = event.data.chat_log_delta;
|
||||
|
||||
// new message
|
||||
if (delta.role) {
|
||||
// If currentDeltaRole exists, it means we're receiving our
|
||||
// second or later message. Let's add it to the chat.
|
||||
if (currentDeltaRole && delta.role && hassMessage.text !== "…") {
|
||||
// Remove progress indicator of previous message
|
||||
hassMessage.text = hassMessage.text.substring(
|
||||
0,
|
||||
hassMessage.text.length - 1
|
||||
);
|
||||
|
||||
hassMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
error: false,
|
||||
};
|
||||
this._addMessage(hassMessage);
|
||||
}
|
||||
currentDeltaRole = delta.role;
|
||||
}
|
||||
|
||||
if (
|
||||
currentDeltaRole === "assistant" &&
|
||||
"content" in delta &&
|
||||
delta.content
|
||||
) {
|
||||
hassMessage.text =
|
||||
hassMessage.text.substring(0, hassMessage.text.length - 1) +
|
||||
delta.content +
|
||||
"…";
|
||||
this.requestUpdate("_conversation");
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "intent-end") {
|
||||
this._conversationId = event.data.intent_output.conversation_id;
|
||||
const plain = event.data.intent_output.response.speech?.plain;
|
||||
@@ -474,71 +435,28 @@ export class HaAssistChat extends LitElement {
|
||||
this._processing = true;
|
||||
this._audio?.pause();
|
||||
this._addMessage({ who: "user", text });
|
||||
let hassMessage = {
|
||||
const message: AssistMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
error: false,
|
||||
};
|
||||
let currentDeltaRole = "";
|
||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||
this._addMessage(hassMessage);
|
||||
this._addMessage(message);
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
this.hass,
|
||||
(event) => {
|
||||
if (event.type === "intent-progress") {
|
||||
const delta = event.data.chat_log_delta;
|
||||
|
||||
// new message and previous message has content
|
||||
if (delta.role) {
|
||||
// If currentDeltaRole exists, it means we're receiving our
|
||||
// second or later message. Let's add it to the chat.
|
||||
if (
|
||||
currentDeltaRole &&
|
||||
delta.role === "assistant" &&
|
||||
hassMessage.text !== "…"
|
||||
) {
|
||||
// Remove progress indicator of previous message
|
||||
hassMessage.text = hassMessage.text.substring(
|
||||
0,
|
||||
hassMessage.text.length - 1
|
||||
);
|
||||
|
||||
hassMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
error: false,
|
||||
};
|
||||
this._addMessage(hassMessage);
|
||||
}
|
||||
currentDeltaRole = delta.role;
|
||||
}
|
||||
|
||||
if (
|
||||
currentDeltaRole === "assistant" &&
|
||||
"content" in delta &&
|
||||
delta.content
|
||||
) {
|
||||
hassMessage.text =
|
||||
hassMessage.text.substring(0, hassMessage.text.length - 1) +
|
||||
delta.content +
|
||||
"…";
|
||||
this.requestUpdate("_conversation");
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "intent-end") {
|
||||
this._conversationId = event.data.intent_output.conversation_id;
|
||||
const plain = event.data.intent_output.response.speech?.plain;
|
||||
if (plain) {
|
||||
hassMessage.text = plain.speech;
|
||||
message.text = plain.speech;
|
||||
}
|
||||
this.requestUpdate("_conversation");
|
||||
unsub();
|
||||
}
|
||||
if (event.type === "error") {
|
||||
hassMessage.text = event.data.message;
|
||||
hassMessage.error = true;
|
||||
message.text = event.data.message;
|
||||
message.error = true;
|
||||
this.requestUpdate("_conversation");
|
||||
unsub();
|
||||
}
|
||||
@@ -552,8 +470,8 @@ export class HaAssistChat extends LitElement {
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
hassMessage.text = this.hass.localize("ui.dialogs.voice_command.error");
|
||||
hassMessage.error = true;
|
||||
message.text = this.hass.localize("ui.dialogs.voice_command.error");
|
||||
message.error = true;
|
||||
this.requestUpdate("_conversation");
|
||||
} finally {
|
||||
this._processing = false;
|
||||
|
@@ -329,12 +329,15 @@ export class HaBaseTimeInput extends LitElement {
|
||||
:host([clearable]) {
|
||||
position: relative;
|
||||
}
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.time-input-wrap-wrap {
|
||||
display: flex;
|
||||
}
|
||||
.time-input-wrap {
|
||||
display: flex;
|
||||
flex: var(--time-input-flex, unset);
|
||||
flex: 1;
|
||||
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
@@ -1,110 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -5,7 +5,6 @@ import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import {
|
||||
addDays,
|
||||
subHours,
|
||||
endOfDay,
|
||||
endOfMonth,
|
||||
endOfWeek,
|
||||
@@ -16,7 +15,6 @@ import {
|
||||
startOfYear,
|
||||
isThisYear,
|
||||
} from "date-fns";
|
||||
import { fromZonedTime, toZonedTime } from "date-fns-tz";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -24,18 +22,16 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { calcDate, shiftDateRange } from "../common/datetime/calc_date";
|
||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||
import {
|
||||
formatShortDateTime,
|
||||
formatShortDateTimeWithYear,
|
||||
formatShortDateTime,
|
||||
} from "../common/datetime/format_date_time";
|
||||
import { useAmPm } from "../common/datetime/use_am_pm";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { TimeZone } from "../data/translation";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./date-range-picker";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-textarea";
|
||||
import "./ha-icon-button-next";
|
||||
import "./ha-icon-button-prev";
|
||||
import "./ha-textarea";
|
||||
|
||||
export type DateRangePickerRanges = Record<string, [Date, Date]>;
|
||||
|
||||
@@ -179,96 +175,6 @@ export class HaDateRangePicker extends LitElement {
|
||||
weekStartsOn,
|
||||
}),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-1h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
1
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-12h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
12
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-24h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-7d"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24 * 7
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-30d"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24 * 30
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
@@ -291,15 +197,14 @@ export class HaDateRangePicker extends LitElement {
|
||||
?auto-apply=${this.autoApply}
|
||||
time-picker=${this.timePicker}
|
||||
twentyfour-hours=${this._hour24format}
|
||||
start-date=${this._formatDate(this.startDate)}
|
||||
end-date=${this._formatDate(this.endDate)}
|
||||
start-date=${this.startDate.toISOString()}
|
||||
end-date=${this.endDate.toISOString()}
|
||||
?ranges=${this.ranges !== false}
|
||||
opening-direction=${ifDefined(
|
||||
this.openingDirection || this._calcedOpeningDirection
|
||||
)}
|
||||
first-day=${firstWeekdayIndex(this.hass.locale)}
|
||||
language=${this.hass.locale.language}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
<div slot="input" class="date-range-inputs" @click=${this._handleClick}>
|
||||
${!this.minimal
|
||||
@@ -420,31 +325,9 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
|
||||
private _applyDateRange() {
|
||||
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||
const dateRangePicker = this._dateRangePicker;
|
||||
|
||||
const startDate = fromZonedTime(
|
||||
dateRangePicker.start,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
const endDate = fromZonedTime(
|
||||
dateRangePicker.end,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
|
||||
dateRangePicker.clickRange([startDate, endDate]);
|
||||
}
|
||||
|
||||
this._dateRangePicker.clickedApply();
|
||||
}
|
||||
|
||||
private _formatDate(date: Date): string {
|
||||
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||
return toZonedTime(date, this.hass.config.time_zone).toISOString();
|
||||
}
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
private get _dateRangePicker() {
|
||||
const dateRangePicker = this.shadowRoot!.querySelector(
|
||||
"date-range-picker"
|
||||
@@ -475,66 +358,45 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const startDate = ev.detail.startDate;
|
||||
const endDate = ev.detail.endDate;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { startDate, endDate },
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-icon-button {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.date-range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
ha-icon-button {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.date-range-ranges {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
}
|
||||
.date-range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-range-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
.date-range-ranges {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.date-range-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
ha-textarea {
|
||||
display: inline-block;
|
||||
width: 340px;
|
||||
}
|
||||
@media only screen and (max-width: 460px) {
|
||||
ha-textarea {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
width: 340px;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-height: 940px) and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
overflow: auto;
|
||||
max-height: calc(70vh - 330px);
|
||||
min-height: 160px;
|
||||
@media only screen and (max-width: 460px) {
|
||||
ha-textarea {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
:host([header-position]) .date-range-ranges {
|
||||
max-height: calc(90vh - 430px);
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -11,7 +11,6 @@ import "./ha-icon-button";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -24,8 +23,6 @@ declare global {
|
||||
export class HaFileUpload extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property() public accept!: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
@@ -34,10 +31,6 @@ export class HaFileUpload extends LitElement {
|
||||
|
||||
@property() public secondary?: string;
|
||||
|
||||
@property({ attribute: "uploading-label" }) public uploadingLabel?: string;
|
||||
|
||||
@property({ attribute: "delete-label" }) public deleteLabel?: string;
|
||||
|
||||
@property() public supports?: string;
|
||||
|
||||
@property({ type: Object }) public value?: File | File[] | FileList | string;
|
||||
@@ -80,22 +73,23 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const localize = this.localize || this.hass!.localize;
|
||||
return html`
|
||||
${this.uploading
|
||||
? html`<div class="container">
|
||||
<div class="uploading">
|
||||
<span class="header"
|
||||
>${this.uploadingLabel || this.value
|
||||
? localize("ui.components.file-upload.uploading_name", {
|
||||
name: this._name,
|
||||
})
|
||||
: localize("ui.components.file-upload.uploading")}</span
|
||||
>${this.value
|
||||
? this.hass?.localize(
|
||||
"ui.components.file-upload.uploading_name",
|
||||
{ name: this._name }
|
||||
)
|
||||
: this.hass?.localize(
|
||||
"ui.components.file-upload.uploading"
|
||||
)}</span
|
||||
>
|
||||
${this.progress
|
||||
? html`<div class="progress">
|
||||
${this.progress}${this.hass &&
|
||||
blankBeforePercent(this.hass!.locale)}%
|
||||
${this.progress}${blankBeforePercent(this.hass!.locale)}%
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -122,11 +116,14 @@ export class HaFileUpload extends LitElement {
|
||||
.path=${this.icon || mdiFileUpload}
|
||||
></ha-svg-icon>
|
||||
<ha-button unelevated @click=${this._openFilePicker}>
|
||||
${this.label || localize("ui.components.file-upload.label")}
|
||||
${this.label ||
|
||||
this.hass?.localize("ui.components.file-upload.label")}
|
||||
</ha-button>
|
||||
<span class="secondary"
|
||||
>${this.secondary ||
|
||||
localize("ui.components.file-upload.secondary")}</span
|
||||
this.hass?.localize(
|
||||
"ui.components.file-upload.secondary"
|
||||
)}</span
|
||||
>
|
||||
<span class="supports">${this.supports}</span>`
|
||||
: typeof this.value === "string"
|
||||
@@ -139,7 +136,8 @@ export class HaFileUpload extends LitElement {
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.deleteLabel || localize("ui.common.delete")}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
@@ -157,8 +155,8 @@ export class HaFileUpload extends LitElement {
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.deleteLabel ||
|
||||
localize("ui.common.delete")}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
@@ -240,10 +238,6 @@ export class HaFileUpload extends LitElement {
|
||||
border-radius: var(--mdc-shape-small, 4px);
|
||||
height: 100%;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
label.container {
|
||||
border: dashed 1px
|
||||
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
||||
|
@@ -80,6 +80,7 @@ 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,33 +1,25 @@
|
||||
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"
|
||||
| "bottom"
|
||||
| "left"
|
||||
| "right"
|
||||
| "top-start"
|
||||
| "top-end"
|
||||
| "right-start"
|
||||
| "right-end"
|
||||
| "bottom-start"
|
||||
| "bottom-end"
|
||||
| "left-start"
|
||||
| "left-end" = "top";
|
||||
@property() public position = "top";
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-tooltip .placement=${this.position} .content=${this.label}>
|
||||
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
|
||||
</ha-tooltip>
|
||||
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
|
||||
<simple-tooltip
|
||||
offset="4"
|
||||
.position=${this.position}
|
||||
.fitToVisibleBounds=${true}
|
||||
>${this.label}</simple-tooltip
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { haStyle } from "../resources/styles";
|
||||
@@ -9,7 +10,6 @@ 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,20 +70,25 @@ 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`<ha-tooltip
|
||||
.disabled=${!item.tooltip}
|
||||
.content=${item.tooltip ?? ""}
|
||||
>
|
||||
: html`<div>
|
||||
${item.tooltip
|
||||
? html`<simple-tooltip
|
||||
animation-delay="0"
|
||||
position="left"
|
||||
>
|
||||
${item.tooltip}
|
||||
</simple-tooltip>`
|
||||
: ""}
|
||||
<ha-icon-button
|
||||
@click=${item.action}
|
||||
.label=${item.label}
|
||||
.path=${item.path}
|
||||
?disabled=${item.disabled}
|
||||
></ha-icon-button>
|
||||
</ha-tooltip>`
|
||||
</div> `
|
||||
)}
|
||||
`}
|
||||
`;
|
||||
|
@@ -1,12 +1,7 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import hash from "object-hash";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { renderMarkdown } from "../resources/render-markdown";
|
||||
import { CacheManager } from "../util/cache-manager";
|
||||
|
||||
const markdownCache = new CacheManager<string>(1000);
|
||||
|
||||
const _gitHubMarkdownAlerts = {
|
||||
reType:
|
||||
@@ -31,16 +26,6 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public cache = false;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
markdownCache.set(key, this.innerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
@@ -52,24 +37,6 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
if (!this.innerHTML && this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
if (markdownCache.has(key)) {
|
||||
this.innerHTML = markdownCache.get(key)!;
|
||||
this._resize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _computeCacheKey() {
|
||||
return hash({
|
||||
content: this.content,
|
||||
allowSvg: this.allowSvg,
|
||||
breaks: this.breaks,
|
||||
});
|
||||
}
|
||||
|
||||
private async _render() {
|
||||
this.innerHTML = await renderMarkdown(
|
||||
String(this.content),
|
||||
|
@@ -13,8 +13,6 @@ export class HaMarkdown extends LitElement {
|
||||
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public cache = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.content) {
|
||||
return nothing;
|
||||
@@ -25,7 +23,6 @@ export class HaMarkdown extends LitElement {
|
||||
.allowSvg=${this.allowSvg}
|
||||
.breaks=${this.breaks}
|
||||
.lazyImages=${this.lazyImages}
|
||||
.cache=${this.cache}
|
||||
></ha-markdown-element>`;
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
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";
|
||||
@@ -63,13 +64,9 @@ export class HaNetwork extends LitElement {
|
||||
>
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading" data-for="auto_configure">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.adapter.auto_configure"
|
||||
)}
|
||||
</span>
|
||||
<span slot="heading" data-for="auto_configure"> Auto Configure </span>
|
||||
<span slot="description" data-for="auto_configure">
|
||||
${this.hass.localize("ui.panel.config.network.adapter.detected")}:
|
||||
Detected:
|
||||
${format_auto_detected_interfaces(this.networkConfig.adapters)}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
@@ -88,21 +85,18 @@ export class HaNetwork extends LitElement {
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.adapter.adapter"
|
||||
)}:
|
||||
${adapter.name}
|
||||
Adapter: ${adapter.name}
|
||||
${adapter.default
|
||||
? html`<ha-svg-icon .path=${mdiStar}></ha-svg-icon>
|
||||
(${this.hass.localize("ui.common.default")})`
|
||||
: nothing}
|
||||
(Default)`
|
||||
: ""}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
)
|
||||
: nothing}
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -132,6 +132,7 @@ 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,11 +250,6 @@ 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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -8,7 +8,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
// and "qr-scanner" defaults to a suboptimal implementation if it is not available.
|
||||
// The following import makes a better implementation available that is based on a
|
||||
// WebAssembly port of ZXing:
|
||||
import { prepareZXingModule } from "barcode-detector";
|
||||
import { setZXingModuleOverrides } from "barcode-detector";
|
||||
import type QrScanner from "qr-scanner";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
@@ -21,14 +21,12 @@ import "./ha-list-item";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
prepareZXingModule({
|
||||
overrides: {
|
||||
locateFile: (path: string, prefix: string) => {
|
||||
if (path.endsWith(".wasm")) {
|
||||
return "/static/js/zxing_reader.wasm";
|
||||
}
|
||||
return prefix + path;
|
||||
},
|
||||
setZXingModuleOverrides({
|
||||
locateFile: (path: string, prefix: string) => {
|
||||
if (path.endsWith(".wasm")) {
|
||||
return "/static/js/zxing_reader.wasm";
|
||||
}
|
||||
return prefix + path;
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -1,222 +0,0 @@
|
||||
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,14 +35,6 @@ 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`
|
||||
@@ -58,7 +50,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}
|
||||
@@ -91,23 +83,6 @@ 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,7 +19,6 @@ 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 {
|
||||
@@ -92,25 +91,6 @@ 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 &&
|
||||
@@ -289,7 +269,7 @@ export class HaSelectSelector extends LitElement {
|
||||
: "";
|
||||
}
|
||||
|
||||
private get _mode(): "list" | "dropdown" | "box" {
|
||||
private get _mode(): "list" | "dropdown" {
|
||||
return (
|
||||
this.selector.select?.mode ||
|
||||
((this.selector.select?.options?.length || 0) < 6 ? "list" : "dropdown")
|
||||
@@ -431,15 +411,6 @@ 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,6 +95,7 @@ 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,8 +20,6 @@ export class HaTabs extends PaperTabs {
|
||||
|
||||
private _lastLeftHiddenState = false;
|
||||
|
||||
private _lastRightHiddenState = false;
|
||||
|
||||
static get template(): HTMLTemplateElement {
|
||||
if (!subTemplate) {
|
||||
subTemplate = (PaperTabs as any).template.cloneNode(true);
|
||||
@@ -87,23 +85,14 @@ export class HaTabs extends PaperTabs {
|
||||
this.$.tabsContainer.scrollLeft += dx;
|
||||
|
||||
const scrollLeft = this.$.tabsContainer.scrollLeft;
|
||||
const dirRTL = this.dir === "rtl";
|
||||
|
||||
const boolCondition1 = Math.abs(scrollLeft) < this._firstTabWidth;
|
||||
const boolCondition2 =
|
||||
Math.abs(scrollLeft) + this._lastTabWidth > this._tabContainerScrollSize;
|
||||
this._leftHidden = scrollLeft - this._firstTabWidth < 0;
|
||||
this._rightHidden =
|
||||
scrollLeft + this._lastTabWidth > this._tabContainerScrollSize;
|
||||
|
||||
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;
|
||||
if (this._lastLeftHiddenState !== this._leftHidden) {
|
||||
this._lastLeftHiddenState = this._leftHidden;
|
||||
this.$.tabsContainer.scrollLeft += this._leftHidden ? -23 : 23;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
// @ts-ignore
|
||||
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
@@ -46,7 +47,6 @@ 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,40 +339,38 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
${type === "entity_id"
|
||||
? ""
|
||||
: html`<span role="gridcell">
|
||||
<ha-tooltip
|
||||
.content=${this.hass.localize(
|
||||
`ui.components.target-picker.expand_${type}`
|
||||
<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(
|
||||
`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-tooltip
|
||||
.content=${this.hass.localize(
|
||||
<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(
|
||||
`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>
|
||||
`;
|
||||
@@ -830,6 +828,9 @@ 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;
|
||||
@@ -843,9 +844,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
ha-tooltip {
|
||||
--ha-tooltip-arrow-size: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -1,41 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -8,10 +8,9 @@ import type {
|
||||
Map,
|
||||
Marker,
|
||||
Polyline,
|
||||
MarkerClusterGroup,
|
||||
} from "leaflet";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, ReactiveElement } from "lit";
|
||||
import { ReactiveElement, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
@@ -27,7 +26,6 @@ import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import { isTouch } from "../../util/is_touch";
|
||||
import "../ha-icon-button";
|
||||
import "./ha-entity-marker";
|
||||
import { DecoratedMarker } from "../../common/map/decorated_marker";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -86,9 +84,6 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
@property({ type: Number }) public zoom = 14;
|
||||
|
||||
@property({ attribute: "cluster-markers", type: Boolean })
|
||||
public clusterMarkers = true;
|
||||
|
||||
@state() private _loaded = false;
|
||||
|
||||
public leafletMap?: Map;
|
||||
@@ -101,12 +96,10 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
private _mapFocusItems: (Marker | Circle)[] = [];
|
||||
|
||||
private _mapZones: DecoratedMarker[] = [];
|
||||
private _mapZones: (Marker | Circle)[] = [];
|
||||
|
||||
private _mapFocusZones: (Marker | Circle)[] = [];
|
||||
|
||||
private _mapCluster: MarkerClusterGroup | undefined;
|
||||
|
||||
private _mapPaths: (Polyline | CircleMarker)[] = [];
|
||||
|
||||
private _clickCount = 0;
|
||||
@@ -158,10 +151,6 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("clusterMarkers")) {
|
||||
this._drawEntities();
|
||||
}
|
||||
|
||||
if (changedProps.has("_loaded") || changedProps.has("paths")) {
|
||||
this._drawPaths();
|
||||
}
|
||||
@@ -186,7 +175,6 @@ export class HaMap extends ReactiveElement {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateMapStyle();
|
||||
}
|
||||
|
||||
@@ -438,11 +426,6 @@ export class HaMap extends ReactiveElement {
|
||||
this._mapFocusZones = [];
|
||||
}
|
||||
|
||||
if (this._mapCluster) {
|
||||
this._mapCluster.remove();
|
||||
this._mapCluster = undefined;
|
||||
}
|
||||
|
||||
if (!this.entities) {
|
||||
return;
|
||||
}
|
||||
@@ -498,24 +481,26 @@ export class HaMap extends ReactiveElement {
|
||||
iconHTML = el.outerHTML;
|
||||
}
|
||||
|
||||
// create marker with the icon
|
||||
this._mapZones.push(
|
||||
Leaflet.marker([latitude, longitude], {
|
||||
icon: Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className,
|
||||
}),
|
||||
interactive: this.interactiveZones,
|
||||
title,
|
||||
})
|
||||
);
|
||||
|
||||
// create circle around it
|
||||
const circle = Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: passive ? passiveZoneColor : zoneColor,
|
||||
radius,
|
||||
});
|
||||
|
||||
const marker = new DecoratedMarker([latitude, longitude], circle, {
|
||||
icon: Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className,
|
||||
}),
|
||||
interactive: this.interactiveZones,
|
||||
title,
|
||||
});
|
||||
|
||||
this._mapZones.push(marker);
|
||||
this._mapZones.push(circle);
|
||||
if (
|
||||
this.fitZones &&
|
||||
(typeof entity === "string" || entity.focus !== false)
|
||||
@@ -553,7 +538,7 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
|
||||
// create marker with the icon
|
||||
const marker = new DecoratedMarker([latitude, longitude], undefined, {
|
||||
const marker = Leaflet.marker([latitude, longitude], {
|
||||
icon: Leaflet.divIcon({
|
||||
html: entityMarker,
|
||||
iconSize: [48, 48],
|
||||
@@ -561,34 +546,24 @@ export class HaMap extends ReactiveElement {
|
||||
}),
|
||||
title: title,
|
||||
});
|
||||
this._mapItems.push(marker);
|
||||
if (typeof entity === "string" || entity.focus !== false) {
|
||||
this._mapFocusItems.push(marker);
|
||||
}
|
||||
|
||||
// create circle around if entity has accuracy
|
||||
if (gpsAccuracy) {
|
||||
marker.decorationLayer = Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: darkPrimaryColor,
|
||||
radius: gpsAccuracy,
|
||||
});
|
||||
this._mapItems.push(
|
||||
Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: darkPrimaryColor,
|
||||
radius: gpsAccuracy,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this._mapItems.push(marker);
|
||||
}
|
||||
|
||||
if (this.clusterMarkers) {
|
||||
this._mapCluster = Leaflet.markerClusterGroup({
|
||||
showCoverageOnHover: false,
|
||||
removeOutsideVisibleBounds: false,
|
||||
maxClusterRadius: 40,
|
||||
});
|
||||
this._mapCluster.addLayers(this._mapItems);
|
||||
map.addLayer(this._mapCluster);
|
||||
} else {
|
||||
this._mapItems.forEach((marker) => map.addLayer(marker));
|
||||
}
|
||||
|
||||
this._mapItems.forEach((marker) => map.addLayer(marker));
|
||||
this._mapZones.forEach((marker) => map.addLayer(marker));
|
||||
}
|
||||
|
||||
@@ -673,22 +648,6 @@ 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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|