mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-20 22:59:29 +00:00
Compare commits
196 Commits
allow-to-s
...
20230906.1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
35496ead23 | ||
![]() |
ba88fef09b | ||
![]() |
ad0e59c8f4 | ||
![]() |
14e6f5e8ca | ||
![]() |
41403a5d35 | ||
![]() |
3c48157793 | ||
![]() |
3a07af6ad2 | ||
![]() |
c1c05f8d22 | ||
![]() |
29aed5371c | ||
![]() |
76c878df57 | ||
![]() |
8acf557137 | ||
![]() |
d6e7ebe71d | ||
![]() |
085b26d5ea | ||
![]() |
32472ca627 | ||
![]() |
c3c4bb4421 | ||
![]() |
f7f1a0c32d | ||
![]() |
d4872b177f | ||
![]() |
5bb8c51d25 | ||
![]() |
4e62370d18 | ||
![]() |
77c08fd00f | ||
![]() |
d8894a0078 | ||
![]() |
4fd9c63633 | ||
![]() |
5e1583f925 | ||
![]() |
5d5894cae6 | ||
![]() |
5417513f49 | ||
![]() |
546ba8f12f | ||
![]() |
a398b37380 | ||
![]() |
321f35f30e | ||
![]() |
82dfb06a04 | ||
![]() |
e666aac1bd | ||
![]() |
9e9a0e377e | ||
![]() |
ba3f9a318b | ||
![]() |
f3b4eefb72 | ||
![]() |
6ac1db6953 | ||
![]() |
1b42189dd6 | ||
![]() |
0d893b3d2b | ||
![]() |
7b167a4d7e | ||
![]() |
8e2f1026e7 | ||
![]() |
fe3a63af80 | ||
![]() |
f90ab60354 | ||
![]() |
5da4e1860a | ||
![]() |
6dcb7f2273 | ||
![]() |
53ae7e5a0c | ||
![]() |
56381f9914 | ||
![]() |
be31aecf00 | ||
![]() |
cc5fffc174 | ||
![]() |
dd8a50af31 | ||
![]() |
c8feded4f2 | ||
![]() |
0d0fe75f4e | ||
![]() |
fb69deb617 | ||
![]() |
c291af5d97 | ||
![]() |
6d63028406 | ||
![]() |
3917739ad2 | ||
![]() |
e98e59a265 | ||
![]() |
16ed60902d | ||
![]() |
6c7efc17c2 | ||
![]() |
d187aa0ac6 | ||
![]() |
9c60a047c1 | ||
![]() |
1825749036 | ||
![]() |
93846a2867 | ||
![]() |
f3ed0160af | ||
![]() |
38b275f7f9 | ||
![]() |
c3a36efaa4 | ||
![]() |
68fa67e77a | ||
![]() |
806cebb024 | ||
![]() |
fa788a8223 | ||
![]() |
dfbaee1649 | ||
![]() |
cfb698d0a6 | ||
![]() |
63c3d6406d | ||
![]() |
d817e92a57 | ||
![]() |
96597b3963 | ||
![]() |
40c7bc08d9 | ||
![]() |
b8cd1760f7 | ||
![]() |
24dd45c8cd | ||
![]() |
e06bd41b5e | ||
![]() |
c0793fad83 | ||
![]() |
e002c5d96c | ||
![]() |
099e317d17 | ||
![]() |
ca1a183512 | ||
![]() |
c1cacf735e | ||
![]() |
515cfdb6d1 | ||
![]() |
3a6cffd6c1 | ||
![]() |
c84a826937 | ||
![]() |
c485e8d03e | ||
![]() |
2ab67328d4 | ||
![]() |
d350c35c4e | ||
![]() |
034ce56da5 | ||
![]() |
ae9fcebfd5 | ||
![]() |
6197b55da8 | ||
![]() |
4e5d57b5f3 | ||
![]() |
7040c6d469 | ||
![]() |
6f99a39b55 | ||
![]() |
7483833dcd | ||
![]() |
38fb48b231 | ||
![]() |
166acee1c6 | ||
![]() |
916a6df39b | ||
![]() |
70f37158fb | ||
![]() |
5011bba20e | ||
![]() |
8897bc703d | ||
![]() |
ea6e7d441a | ||
![]() |
f91396c986 | ||
![]() |
4598b530af | ||
![]() |
dfabb4bc36 | ||
![]() |
66e0100c95 | ||
![]() |
a08a989ef5 | ||
![]() |
000c28abf9 | ||
![]() |
6b67397c83 | ||
![]() |
f68823a09e | ||
![]() |
fc1782e676 | ||
![]() |
b4975344a1 | ||
![]() |
2dc08d782f | ||
![]() |
ed92958735 | ||
![]() |
5ce31f3177 | ||
![]() |
370ec9cd98 | ||
![]() |
52c12b5659 | ||
![]() |
3de4cfbc00 | ||
![]() |
4215854414 | ||
![]() |
88eba92f57 | ||
![]() |
f773c968f9 | ||
![]() |
bbb6fccaec | ||
![]() |
aa2b2b0d16 | ||
![]() |
5cc06ebf0b | ||
![]() |
85977e505b | ||
![]() |
3249a5225f | ||
![]() |
7e7205627a | ||
![]() |
d33430e53f | ||
![]() |
e3f53e90e2 | ||
![]() |
811edfcc0f | ||
![]() |
2483249b5f | ||
![]() |
3534617f81 | ||
![]() |
216a3c4c7e | ||
![]() |
567bd9831f | ||
![]() |
98d1a55d35 | ||
![]() |
92358b4859 | ||
![]() |
eca3ec7f98 | ||
![]() |
bfcdbbd70b | ||
![]() |
e764076b1a | ||
![]() |
693c77ce1c | ||
![]() |
a725b6c9de | ||
![]() |
014bbf12ce | ||
![]() |
07dceb8e6d | ||
![]() |
53f18bec53 | ||
![]() |
ac3e858738 | ||
![]() |
c76b2fb357 | ||
![]() |
5f015ac9af | ||
![]() |
ac7c354bfc | ||
![]() |
dddee87de3 | ||
![]() |
e8bd77a84e | ||
![]() |
46a036ddbe | ||
![]() |
bf912f7bd3 | ||
![]() |
196c15ff3e | ||
![]() |
d0a6e727f2 | ||
![]() |
09697148cf | ||
![]() |
76093d898d | ||
![]() |
00c69c0fc3 | ||
![]() |
93dd119ce5 | ||
![]() |
e4f3211e9f | ||
![]() |
c6ecdc9d5d | ||
![]() |
6bdd2d234d | ||
![]() |
9d169bcbeb | ||
![]() |
5c06ec1084 | ||
![]() |
38a317b7e7 | ||
![]() |
593b176ab8 | ||
![]() |
1a15c8da8c | ||
![]() |
060e67397a | ||
![]() |
d6de29ca8a | ||
![]() |
220767b347 | ||
![]() |
79e1fbe076 | ||
![]() |
cd19894ab0 | ||
![]() |
705b6aeb4b | ||
![]() |
6e27fbe10f | ||
![]() |
bbb99a6eee | ||
![]() |
8411efc1c3 | ||
![]() |
88721df637 | ||
![]() |
265faddfa9 | ||
![]() |
6584dc70b7 | ||
![]() |
d6b4dbe6a2 | ||
![]() |
d579f93aa7 | ||
![]() |
b91261a789 | ||
![]() |
872128d9a8 | ||
![]() |
4972db4648 | ||
![]() |
821cd7fe05 | ||
![]() |
8c24ffa710 | ||
![]() |
d50a130345 | ||
![]() |
ee8997fbd2 | ||
![]() |
613cf932b5 | ||
![]() |
12b61aea2f | ||
![]() |
51d9271c83 | ||
![]() |
bd5264308f | ||
![]() |
2c17d2fead | ||
![]() |
9f0b9782a0 | ||
![]() |
88ff4c2fa8 | ||
![]() |
cba246fc7f | ||
![]() |
7d80eb06b0 | ||
![]() |
a181189a49 | ||
![]() |
626b51112f |
8
.github/workflows/cast_deployment.yaml
vendored
8
.github/workflows/cast_deployment.yaml
vendored
@@ -21,12 +21,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -57,12 +57,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
16
.github/workflows/ci.yaml
vendored
16
.github/workflows/ci.yaml
vendored
@@ -24,9 +24,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -55,9 +55,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -73,9 +73,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -91,9 +91,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
8
.github/workflows/demo_deployment.yaml
vendored
8
.github/workflows/demo_deployment.yaml
vendored
@@ -22,12 +22,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -58,12 +58,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/design_deployment.yaml
vendored
4
.github/workflows/design_deployment.yaml
vendored
@@ -16,10 +16,10 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/design_preview.yaml
vendored
4
.github/workflows/design_preview.yaml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v4
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
File diff suppressed because one or more lines are too long
@@ -8,4 +8,4 @@ plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.6.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-3.6.3.cjs
|
||||
|
@@ -8,7 +8,7 @@ module.exports.sourceMapURL = () => {
|
||||
const ref = env.version().endsWith("dev")
|
||||
? process.env.GITHUB_SHA || "dev"
|
||||
: env.version();
|
||||
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}`;
|
||||
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}/`;
|
||||
};
|
||||
|
||||
// Files from NPM Packages that should not be imported
|
||||
|
@@ -1,10 +1,14 @@
|
||||
import fs from "fs/promises";
|
||||
import gulp from "gulp";
|
||||
import path from "path";
|
||||
import mapStream from "map-stream";
|
||||
import transform from "gulp-json-transform";
|
||||
import { LokaliseApi } from "@lokalise/node-api";
|
||||
import JSZip from "jszip";
|
||||
|
||||
const inDirFrontend = "translations/frontend";
|
||||
const inDirBackend = "translations/backend";
|
||||
const inDir = "translations";
|
||||
const inDirFrontend = `${inDir}/frontend`;
|
||||
const inDirBackend = `${inDir}/backend`;
|
||||
const srcMeta = "src/translations/translationMetadata.json";
|
||||
const encoding = "utf8";
|
||||
|
||||
@@ -68,8 +72,9 @@ gulp.task("convert-backend-translations", function () {
|
||||
});
|
||||
|
||||
gulp.task("check-translations-html", function () {
|
||||
// We exclude backend translations because they are not compliant with the HTML rule for now
|
||||
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml());
|
||||
return gulp
|
||||
.src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`])
|
||||
.pipe(checkHtml());
|
||||
});
|
||||
|
||||
gulp.task("check-all-files-exist", async function () {
|
||||
@@ -89,7 +94,83 @@ gulp.task("check-all-files-exist", async function () {
|
||||
await Promise.allSettled(writings);
|
||||
});
|
||||
|
||||
const lokaliseProjects = {
|
||||
backend: "130246255a974bd3b5e8a1.51616605",
|
||||
frontend: "3420425759f6d6d241f598.13594006",
|
||||
};
|
||||
|
||||
gulp.task("fetch-lokalise", async function () {
|
||||
let apiKey;
|
||||
try {
|
||||
apiKey =
|
||||
process.env.LOKALISE_TOKEN ||
|
||||
(await fs.readFile(".lokalise_token", { encoding }));
|
||||
} catch {
|
||||
throw new Error(
|
||||
"An Administrator Lokalise API token is required to download the latest set of translations. Place your token in a new file `.lokalise_token` in the repo root directory."
|
||||
);
|
||||
}
|
||||
const lokaliseApi = new LokaliseApi({ apiKey });
|
||||
|
||||
const mkdirPromise = Promise.all([
|
||||
fs.mkdir(inDirFrontend, { recursive: true }),
|
||||
fs.mkdir(inDirBackend, { recursive: true }),
|
||||
]);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(lokaliseProjects).map(([project, projectId]) =>
|
||||
lokaliseApi
|
||||
.files()
|
||||
.download(projectId, {
|
||||
format: "json",
|
||||
original_filenames: false,
|
||||
replace_breaks: false,
|
||||
json_unescaped_slashes: true,
|
||||
export_empty_as: "skip",
|
||||
})
|
||||
.then((download) => fetch(download.bundle_url))
|
||||
.then((response) => {
|
||||
if (response.status === 200 || response.status === 0) {
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
throw new Error(response.statusText);
|
||||
})
|
||||
.then(JSZip.loadAsync)
|
||||
.then(async (contents) => {
|
||||
await mkdirPromise;
|
||||
return Promise.all(
|
||||
Object.keys(contents.files).map(async (filename) => {
|
||||
const file = contents.file(filename);
|
||||
if (!file) {
|
||||
// no file, probably a directory
|
||||
return Promise.resolve();
|
||||
}
|
||||
return file
|
||||
.async("nodebuffer")
|
||||
.then((content) =>
|
||||
fs.writeFile(
|
||||
path.join(
|
||||
inDir,
|
||||
project,
|
||||
filename.split("/").splice(-1)[0]
|
||||
),
|
||||
content,
|
||||
{ flag: "w", encoding }
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"check-downloaded-translations",
|
||||
gulp.series("check-translations-html", "check-all-files-exist")
|
||||
"download-translations",
|
||||
gulp.series(
|
||||
"fetch-lokalise",
|
||||
"convert-backend-translations",
|
||||
"check-translations-html",
|
||||
"check-all-files-exist"
|
||||
)
|
||||
);
|
||||
|
@@ -1,5 +1,6 @@
|
||||
const webpack = require("webpack");
|
||||
const { existsSync } = require("fs");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||
const log = require("fancy-log");
|
||||
@@ -191,19 +192,26 @@ const createWebpackConfig = ({
|
||||
// Since production source maps don't include sources, we need to point to them elsewhere
|
||||
// For dependencies, just provide the path (no source in browser)
|
||||
// Otherwise, point to the raw code on GitHub for browser to load
|
||||
devtoolModuleFilenameTemplate:
|
||||
!isTestBuild && isProdBuild
|
||||
? (info) => {
|
||||
const sourcePath = info.resourcePath.replace(/^\.\//, "");
|
||||
if (
|
||||
sourcePath.startsWith("node_modules") ||
|
||||
sourcePath.startsWith("webpack")
|
||||
) {
|
||||
return `no-source/${sourcePath}`;
|
||||
...Object.fromEntries(
|
||||
["", "Fallback"].map((v) => [
|
||||
`devtool${v}ModuleFilenameTemplate`,
|
||||
!isTestBuild && isProdBuild
|
||||
? (info) => {
|
||||
if (
|
||||
!path.isAbsolute(info.absoluteResourcePath) ||
|
||||
!existsSync(info.resourcePath) ||
|
||||
info.resourcePath.startsWith("./node_modules")
|
||||
) {
|
||||
// Source URLs are unknown for dependencies, so we use a relative URL with a
|
||||
// non - existent top directory. This results in a clean source tree in browser
|
||||
// dev tools, and they stay happy getting 404s with valid requests.
|
||||
return `/unknown${path.resolve("/", info.resourcePath)}`;
|
||||
}
|
||||
return new URL(info.resourcePath, bundle.sourceMapURL()).href;
|
||||
}
|
||||
return `${bundle.sourceMapURL()}/${sourcePath}`;
|
||||
}
|
||||
: undefined,
|
||||
: undefined,
|
||||
])
|
||||
),
|
||||
},
|
||||
experiments: {
|
||||
outputModule: true,
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||
@@ -7,6 +6,7 @@ import "../../../src/components/ha-switch";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "./demo-card";
|
||||
import type { DemoCardConfig } from "./demo-card";
|
||||
import "../ha-demo-options";
|
||||
|
||||
@customElement("demo-cards")
|
||||
class DemoCards extends LitElement {
|
||||
@@ -20,20 +20,14 @@ class DemoCards extends LitElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<app-toolbar>
|
||||
<div class="filters">
|
||||
<ha-formfield label="Show config">
|
||||
<ha-switch
|
||||
.checked=${this._showConfig}
|
||||
@change=${this._showConfigToggled}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Dark theme">
|
||||
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
</app-toolbar>
|
||||
<ha-demo-options>
|
||||
<ha-formfield label="Show config">
|
||||
<ha-switch @change=${this._showConfigToggled}> </ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Dark theme">
|
||||
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
|
||||
</ha-formfield>
|
||||
</ha-demo-options>
|
||||
<div id="container">
|
||||
<div class="cards">
|
||||
${this.configs.map(
|
||||
@@ -69,12 +63,6 @@ class DemoCards extends LitElement {
|
||||
demo-card {
|
||||
margin: 16px 16px 32px;
|
||||
}
|
||||
app-toolbar {
|
||||
background-color: var(--light-primary-color);
|
||||
}
|
||||
.filters {
|
||||
margin-left: 60px;
|
||||
}
|
||||
ha-formfield {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
@@ -1,93 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/dialogs/more-info/more-info-content";
|
||||
import "../../../src/state-summary/state-card-content";
|
||||
|
||||
class DemoMoreInfo extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
.root {
|
||||
display: flex;
|
||||
}
|
||||
#card {
|
||||
max-width: 400px;
|
||||
width: 100vw;
|
||||
}
|
||||
ha-card {
|
||||
width: 352px;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
state-card-content {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
pre {
|
||||
width: 400px;
|
||||
margin: 0 16px;
|
||||
overflow: auto;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@media only screen and (max-width: 800px) {
|
||||
.root {
|
||||
flex-direction: column;
|
||||
}
|
||||
pre {
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="root">
|
||||
<div id="card">
|
||||
<ha-card>
|
||||
<state-card-content
|
||||
state-obj="[[_stateObj]]"
|
||||
hass="[[hass]]"
|
||||
in-dialog
|
||||
></state-card-content>
|
||||
|
||||
<more-info-content
|
||||
hass="[[hass]]"
|
||||
state-obj="[[_stateObj]]"
|
||||
></more-info-content>
|
||||
</ha-card>
|
||||
</div>
|
||||
<template is="dom-if" if="[[showConfig]]">
|
||||
<pre>[[_jsonEntity(_stateObj)]]</pre>
|
||||
</template>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
entityId: String,
|
||||
showConfig: Boolean,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_getState(entityId, hass.states)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_getState(entityId, states) {
|
||||
return states[entityId];
|
||||
}
|
||||
|
||||
_jsonEntity(stateObj) {
|
||||
// We are caching some things on stateObj
|
||||
// (it sucks, we will remove in the future)
|
||||
const tmp = {};
|
||||
Object.keys(stateObj).forEach((key) => {
|
||||
if (key[0] !== "_") {
|
||||
tmp[key] = stateObj[key];
|
||||
}
|
||||
});
|
||||
return JSON.stringify(tmp, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-more-info", DemoMoreInfo);
|
93
gallery/src/components/demo-more-info.ts
Normal file
93
gallery/src/components/demo-more-info.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/dialogs/more-info/more-info-content";
|
||||
import "../../../src/state-summary/state-card-content";
|
||||
import "../ha-demo-options";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
|
||||
@customElement("demo-more-info")
|
||||
class DemoMoreInfo extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public entityId!: string;
|
||||
|
||||
@property() public showConfig!: boolean;
|
||||
|
||||
render() {
|
||||
const state = this._getState(this.entityId, this.hass.states);
|
||||
return html`
|
||||
<div class="root">
|
||||
<div id="card">
|
||||
<ha-card>
|
||||
<state-card-content
|
||||
.stateObj=${state}
|
||||
.hass=${this.hass}
|
||||
in-dialog
|
||||
></state-card-content>
|
||||
|
||||
<more-info-content
|
||||
.hass=${this.hass}
|
||||
.stateObj=${state}
|
||||
></more-info-content>
|
||||
</ha-card>
|
||||
</div>
|
||||
${this.showConfig ? html`<pre>${this._jsonEntity(state)}</pre>` : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getState(entityId, states) {
|
||||
return states[entityId];
|
||||
}
|
||||
|
||||
private _jsonEntity(stateObj) {
|
||||
// We are caching some things on stateObj
|
||||
// (it sucks, we will remove in the future)
|
||||
const tmp = {};
|
||||
Object.keys(stateObj).forEach((key) => {
|
||||
if (key[0] !== "_") {
|
||||
tmp[key] = stateObj[key];
|
||||
}
|
||||
});
|
||||
return JSON.stringify(tmp, null, 2);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.root {
|
||||
display: flex;
|
||||
}
|
||||
#card {
|
||||
max-width: 400px;
|
||||
width: 100vw;
|
||||
}
|
||||
ha-card {
|
||||
width: 352px;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
state-card-content {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
pre {
|
||||
width: 400px;
|
||||
margin: 0 16px;
|
||||
overflow: auto;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@media only screen and (max-width: 800px) {
|
||||
.root {
|
||||
flex-direction: column;
|
||||
}
|
||||
pre {
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info": DemoMoreInfo;
|
||||
}
|
||||
}
|
@@ -1,83 +0,0 @@
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-switch";
|
||||
import "./demo-more-info";
|
||||
|
||||
class DemoMoreInfos extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
#container {
|
||||
min-height: calc(100vh - 128px);
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
demo-more-info {
|
||||
margin: 16px 16px 32px;
|
||||
}
|
||||
app-toolbar {
|
||||
background-color: var(--light-primary-color);
|
||||
}
|
||||
.filters {
|
||||
margin-left: 60px;
|
||||
}
|
||||
ha-formfield {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
<app-toolbar>
|
||||
<div class="filters">
|
||||
<ha-formfield label="Show entities">
|
||||
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Dark theme">
|
||||
<ha-switch on-change="_darkThemeToggled"> </ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
</app-toolbar>
|
||||
<div id="container">
|
||||
<div class="cards">
|
||||
<template is="dom-repeat" items="[[entities]]">
|
||||
<demo-more-info
|
||||
entity-id="[[item]]"
|
||||
show-config="[[_showConfig]]"
|
||||
hass="[[hass]]"
|
||||
></demo-more-info>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
entities: Array,
|
||||
hass: Object,
|
||||
_showConfig: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_showConfigToggled(ev) {
|
||||
this._showConfig = ev.target.checked;
|
||||
}
|
||||
|
||||
_darkThemeToggled(ev) {
|
||||
applyThemesOnElement(this.$.container, { themes: {} }, "default", {
|
||||
dark: ev.target.checked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-more-infos", DemoMoreInfos);
|
87
gallery/src/components/demo-more-infos.ts
Normal file
87
gallery/src/components/demo-more-infos.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-switch";
|
||||
import "./demo-more-info";
|
||||
import "../ha-demo-options";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
|
||||
@customElement("demo-more-infos")
|
||||
class DemoMoreInfos extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public entities!: [];
|
||||
|
||||
@property({ attribute: false }) _showConfig: boolean = false;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ha-demo-options>
|
||||
<ha-formfield label="Show config">
|
||||
<ha-switch @change=${this._showConfigToggled}> </ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Dark theme">
|
||||
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
|
||||
</ha-formfield>
|
||||
</ha-demo-options>
|
||||
<div id="container">
|
||||
<div class="cards">
|
||||
${this.entities.map(
|
||||
(item) =>
|
||||
html`<demo-more-info
|
||||
.entityId=${item}
|
||||
.showConfig=${this._showConfig}
|
||||
.hass=${this.hass}
|
||||
></demo-more-info>`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
#container {
|
||||
min-height: calc(100vh - 128px);
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
demo-more-info {
|
||||
margin: 16px 16px 32px;
|
||||
}
|
||||
ha-formfield {
|
||||
margin-right: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
_showConfigToggled(ev) {
|
||||
this._showConfig = ev.target.checked;
|
||||
}
|
||||
|
||||
_darkThemeToggled(ev) {
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector("#container"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
theme: "default",
|
||||
},
|
||||
"default",
|
||||
{
|
||||
dark: ev.target.checked,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-infos": DemoMoreInfos;
|
||||
}
|
||||
}
|
47
gallery/src/ha-demo-options.ts
Normal file
47
gallery/src/ha-demo-options.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import "@material/mwc-drawer";
|
||||
import "@material/mwc-top-app-bar-fixed";
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../src/components/ha-icon-button";
|
||||
import "../../src/managers/notification-manager";
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
import "./components/page-description";
|
||||
|
||||
@customElement("ha-demo-options")
|
||||
class HaDemoOptions extends LitElement {
|
||||
render() {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background-color: var(--light-primary-color);
|
||||
margin-left: 60px
|
||||
margin-right: 60px;
|
||||
display: var(--layout-horizontal_-_display);
|
||||
-ms-flex-direction: var(--layout-horizontal_-_-ms-flex-direction);
|
||||
-webkit-flex-direction: var(
|
||||
--layout-horizontal_-_-webkit-flex-direction
|
||||
);
|
||||
flex-direction: var(--layout-horizontal_-_flex-direction);
|
||||
-ms-flex-align: var(--layout-center_-_-ms-flex-align);
|
||||
-webkit-align-items: var(--layout-center_-_-webkit-align-items);
|
||||
align-items: var(--layout-center_-_align-items);
|
||||
position: relative;
|
||||
height: 64px;
|
||||
padding: 0 16px;
|
||||
pointer-events: none;
|
||||
font-size: 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-demo-options": HaDemoOptions;
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Control Number Buttons
|
||||
---
|
100
gallery/src/pages/components/ha-control-number-buttons.ts
Normal file
100
gallery/src/pages/components/ha-control-number-buttons.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-number-buttons";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
|
||||
const buttons: {
|
||||
id: string;
|
||||
label: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
class?: string;
|
||||
}[] = [
|
||||
{
|
||||
id: "basic",
|
||||
label: "Basic",
|
||||
},
|
||||
{
|
||||
id: "min_max_step",
|
||||
label: "With min/max and step",
|
||||
min: 5,
|
||||
max: 25,
|
||||
step: 0.5,
|
||||
},
|
||||
{
|
||||
id: "custom",
|
||||
label: "Custom",
|
||||
class: "custom",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-control-number-buttons")
|
||||
export class DemoHarControlNumberButtons extends LitElement {
|
||||
@state() value = 5;
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this.value = ev.detail.value;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${repeat(buttons, (button) => {
|
||||
const { id, label, ...config } = button;
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<label id=${id}>${label}</label>
|
||||
<pre>Config: ${JSON.stringify(config)}</pre>
|
||||
<ha-control-number-buttons
|
||||
.value=${this.value}
|
||||
.min=${config.min}
|
||||
.max=${config.max}
|
||||
.step=${config.step}
|
||||
class=${ifDefined(config.class)}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${label}
|
||||
>
|
||||
</ha-control-number-buttons>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
})}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: 600;
|
||||
}
|
||||
.custom {
|
||||
color: #2196f3;
|
||||
--control-number-buttons-color: #2196f3;
|
||||
--control-number-buttons-background-color: #2196f3;
|
||||
--control-number-buttons-background-opacity: 0.1;
|
||||
--control-number-buttons-thickness: 100px;
|
||||
--control-number-buttons-border-radius: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-control-number-buttons": DemoHarControlNumberButtons;
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Control Select Menu
|
||||
---
|
146
gallery/src/pages/components/ha-control-select-menu.ts
Normal file
146
gallery/src/pages/components/ha-control-select-menu.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { mdiFan, mdiFanSpeed1, mdiFanSpeed2, mdiFanSpeed3 } from "@mdi/js";
|
||||
import { LitElement, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select-menu";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
|
||||
type SelectMenuOptions = {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
type SelectMenu = {
|
||||
label: string;
|
||||
icon: string;
|
||||
class?: string;
|
||||
disabled?: boolean;
|
||||
options: SelectMenuOptions[];
|
||||
};
|
||||
|
||||
const selects: SelectMenu[] = [
|
||||
{
|
||||
label: "Basic select",
|
||||
icon: mdiFan,
|
||||
options: [
|
||||
{
|
||||
value: "low",
|
||||
label: "Low",
|
||||
},
|
||||
{
|
||||
value: "medium",
|
||||
label: "Medium",
|
||||
},
|
||||
{
|
||||
value: "high",
|
||||
label: "High",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Select with icons",
|
||||
icon: mdiFan,
|
||||
options: [
|
||||
{
|
||||
value: "low",
|
||||
label: "Low",
|
||||
icon: mdiFanSpeed1,
|
||||
},
|
||||
{
|
||||
value: "medium",
|
||||
label: "Medium",
|
||||
icon: mdiFanSpeed2,
|
||||
},
|
||||
{
|
||||
value: "high",
|
||||
label: "High",
|
||||
icon: mdiFanSpeed3,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Disabled select",
|
||||
icon: mdiFan,
|
||||
options: [],
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-control-select-menu")
|
||||
export class DemoHaControlSelectMenu extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card>
|
||||
${repeat(
|
||||
selects,
|
||||
(select) => html`
|
||||
<div class="card-content">
|
||||
<ha-control-select-menu
|
||||
.label=${select.label}
|
||||
?disabled=${select.disabled}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${select.icon}></ha-svg-icon>
|
||||
${select.options.map(
|
||||
(option) => html`
|
||||
<ha-list-item
|
||||
.value=${option.value}
|
||||
.graphic=${option.icon ? "icon" : undefined}
|
||||
>
|
||||
${option.icon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${option.icon}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${option.label ?? option.value}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-control-select-menu>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: 600;
|
||||
}
|
||||
.custom {
|
||||
--control-button-icon-color: var(--primary-color);
|
||||
--control-button-background-color: var(--primary-color);
|
||||
--control-button-background-opacity: 0.2;
|
||||
--control-button-border-radius: 18px;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-control-select-menu": DemoHaControlSelectMenu;
|
||||
}
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Temp Color Picker
|
||||
---
|
@@ -1,117 +0,0 @@
|
||||
import "../../../../src/components/ha-temp-color-picker";
|
||||
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-slider";
|
||||
|
||||
@customElement("demo-components-ha-temp-color-picker")
|
||||
export class DemoHaTempColorPicker extends LitElement {
|
||||
@state()
|
||||
min = 3000;
|
||||
|
||||
@state()
|
||||
max = 7000;
|
||||
|
||||
@state()
|
||||
value = 4000;
|
||||
|
||||
@state()
|
||||
liveValue?: number;
|
||||
|
||||
private _minChanged(ev) {
|
||||
this.min = Number(ev.target.value);
|
||||
}
|
||||
|
||||
private _maxChanged(ev) {
|
||||
this.max = Number(ev.target.value);
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this.value = Number(ev.target.value);
|
||||
}
|
||||
|
||||
private _tempColorCursor(ev) {
|
||||
this.liveValue = ev.detail.value;
|
||||
}
|
||||
|
||||
private _tempColorChanged(ev) {
|
||||
this.value = ev.detail.value;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="value">${this.liveValue ?? this.value} K</p>
|
||||
<ha-temp-color-picker
|
||||
.min=${this.min}
|
||||
.max=${this.max}
|
||||
.value=${this.value}
|
||||
@value-changed=${this._tempColorChanged}
|
||||
@cursor-moved=${this._tempColorCursor}
|
||||
></ha-temp-color-picker>
|
||||
<p>Min temp : ${this.min} K</p>
|
||||
<ha-slider
|
||||
step="1"
|
||||
pin
|
||||
min="2000"
|
||||
max="10000"
|
||||
.value=${this.min}
|
||||
@change=${this._minChanged}
|
||||
>
|
||||
</ha-slider>
|
||||
<p>Max temp : ${this.max} K</p>
|
||||
<ha-slider
|
||||
step="1"
|
||||
pin
|
||||
min="2000"
|
||||
max="10000"
|
||||
.value=${this.max}
|
||||
@change=${this._maxChanged}
|
||||
>
|
||||
</ha-slider>
|
||||
<p>Value : ${this.value} K</p>
|
||||
<ha-slider
|
||||
step="1"
|
||||
pin
|
||||
min=${this.min}
|
||||
max=${this.max}
|
||||
.value=${this.value}
|
||||
@change=${this._valueChanged}
|
||||
>
|
||||
</ha-slider>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-temp-color-picker {
|
||||
width: 400px;
|
||||
}
|
||||
.value {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-temp-color-picker": DemoHaTempColorPicker;
|
||||
}
|
||||
}
|
@@ -20,7 +20,7 @@ We want to make it as easy for designers to contribute as it is for developers.
|
||||
|
||||
- Meet us at <a href="https://discord.gg/BPBc8rZ9" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
|
||||
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Find the lates UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
|
||||
## Developers
|
||||
|
||||
|
@@ -14,7 +14,7 @@ const ENTITIES = [
|
||||
}),
|
||||
getEntity("light", "bed_light", "on", {
|
||||
friendly_name: "Bed Light",
|
||||
supported_color_modes: [LightColorMode.HS],
|
||||
supported_color_modes: [LightColorMode.HS, LightColorMode.COLOR_TEMP],
|
||||
}),
|
||||
getEntity("light", "unavailable", "unavailable", {
|
||||
friendly_name: "Unavailable entity",
|
||||
@@ -116,6 +116,15 @@ const CONFIGS = [
|
||||
- type: "light-brightness"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Light color temperature feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: light.bed_light
|
||||
features:
|
||||
- type: "color-temp"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Vacuum commands feature",
|
||||
config: `
|
||||
|
@@ -284,6 +284,13 @@ const ENTITIES: HassEntity[] = [
|
||||
installed_version: "1.0.0",
|
||||
latest_version: "2.0.0",
|
||||
}),
|
||||
createEntity("water_heater.off", "off"),
|
||||
createEntity("water_heater.eco", "eco"),
|
||||
createEntity("water_heater.electric", "electric"),
|
||||
createEntity("water_heater.performance", "performance"),
|
||||
createEntity("water_heater.high_demand", "high_demand"),
|
||||
createEntity("water_heater.heat_pump", "heat_pump"),
|
||||
createEntity("water_heater.gas", "gas"),
|
||||
];
|
||||
|
||||
function createEntity(
|
||||
|
@@ -43,6 +43,28 @@ const ENTITIES = [
|
||||
target_temp_low: 20,
|
||||
target_temp_high: 25,
|
||||
}),
|
||||
getEntity("climate", "advanced", "auto", {
|
||||
friendly_name: "Advanced hvac",
|
||||
supported_features:
|
||||
// eslint-disable-next-line no-bitwise
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE |
|
||||
ClimateEntityFeature.TARGET_HUMIDITY |
|
||||
ClimateEntityFeature.PRESET_MODE,
|
||||
hvac_modes: ["auto", "off"],
|
||||
hvac_mode: "auto",
|
||||
preset_modes: ["eco", "comfort", "boost"],
|
||||
preset_mode: "eco",
|
||||
current_temperature: 18,
|
||||
min_temp: 10,
|
||||
max_temp: 30,
|
||||
target_temp_step: 1,
|
||||
target_temp_low: 20,
|
||||
target_temp_high: 25,
|
||||
current_humidity: 40,
|
||||
min_humidity: 0,
|
||||
max_humidity: 100,
|
||||
humidity: 50,
|
||||
}),
|
||||
getEntity("climate", "unavailable", "unavailable", {
|
||||
friendly_name: "Unavailable heater",
|
||||
hvac_modes: ["heat", "off"],
|
||||
|
3
gallery/src/pages/more-info/water-heater.markdown
Normal file
3
gallery/src/pages/more-info/water-heater.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Water Heater
|
||||
---
|
70
gallery/src/pages/more-info/water-heater.ts
Normal file
70
gallery/src/pages/more-info/water-heater.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import { WaterHeaterEntityFeature } from "../../../../src/data/water_heater";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import {
|
||||
MockHomeAssistant,
|
||||
provideHass,
|
||||
} from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("water_heater", "basic", "eco", {
|
||||
friendly_name: "Basic heater",
|
||||
operation_list: ["heat_pump", "eco", "performance", "off"],
|
||||
operation_mode: "eco",
|
||||
away_mode: "off",
|
||||
target_temp_step: 1,
|
||||
current_temperature: 55,
|
||||
temperature: 60,
|
||||
min_temp: 20,
|
||||
max_temp: 70,
|
||||
supported_features:
|
||||
// eslint-disable-next-line no-bitwise
|
||||
WaterHeaterEntityFeature.TARGET_TEMPERATURE |
|
||||
WaterHeaterEntityFeature.OPERATION_MODE |
|
||||
WaterHeaterEntityFeature.AWAY_MODE,
|
||||
}),
|
||||
getEntity("water_heater", "unavailable", "unavailable", {
|
||||
friendly_name: "Unavailable heater",
|
||||
operation_list: ["heat_pump", "eco", "performance", "off"],
|
||||
operation_mode: "off",
|
||||
min_temp: 20,
|
||||
max_temp: 70,
|
||||
supported_features:
|
||||
// eslint-disable-next-line no-bitwise
|
||||
WaterHeaterEntityFeature.TARGET_TEMPERATURE |
|
||||
WaterHeaterEntityFeature.OPERATION_MODE,
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-water-heater")
|
||||
class DemoMoreInfoWaterHeater extends LitElement {
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-water-heater": DemoMoreInfoWaterHeater;
|
||||
}
|
||||
}
|
@@ -31,8 +31,8 @@ export class HassioUploadBackup extends LitElement {
|
||||
.icon=${mdiFolderUpload}
|
||||
accept="application/x-tar"
|
||||
label="Upload backup"
|
||||
supports="Supports .TAR files"
|
||||
@file-picked=${this._uploadFile}
|
||||
auto-open-file-dialog
|
||||
></ha-file-upload>
|
||||
`;
|
||||
}
|
||||
|
@@ -173,6 +173,7 @@ class HassioBackupDialog
|
||||
private async _restoreClicked() {
|
||||
const backupDetails = this._backupContent.backupDetails();
|
||||
this._restoringBackup = true;
|
||||
this._dialogParams?.onRestoring?.();
|
||||
if (this._backupContent.backupType === "full") {
|
||||
await this._fullRestoreClicked(backupDetails);
|
||||
} else {
|
||||
@@ -219,7 +220,7 @@ class HassioBackupDialog
|
||||
this._error = error.body.message;
|
||||
}
|
||||
} else {
|
||||
fireEvent(this, "restoring");
|
||||
this._dialogParams?.onRestoring?.();
|
||||
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(backupDetails),
|
||||
@@ -268,7 +269,7 @@ class HassioBackupDialog
|
||||
}
|
||||
);
|
||||
} else {
|
||||
fireEvent(this, "restoring");
|
||||
this._dialogParams?.onRestoring?.();
|
||||
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(backupDetails),
|
||||
|
@@ -5,6 +5,7 @@ import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
export interface HassioBackupDialogParams {
|
||||
slug: string;
|
||||
onDelete?: () => void;
|
||||
onRestoring?: () => void;
|
||||
onboarding?: boolean;
|
||||
supervisor?: Supervisor;
|
||||
localize?: LocalizeFunc;
|
||||
|
90
package.json
90
package.json
@@ -25,15 +25,15 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.22.10",
|
||||
"@babel/runtime": "7.22.11",
|
||||
"@braintree/sanitize-url": "6.0.4",
|
||||
"@codemirror/autocomplete": "6.9.0",
|
||||
"@codemirror/commands": "6.2.4",
|
||||
"@codemirror/language": "6.8.0",
|
||||
"@codemirror/commands": "6.2.5",
|
||||
"@codemirror/language": "6.9.0",
|
||||
"@codemirror/legacy-modes": "6.3.3",
|
||||
"@codemirror/search": "6.5.1",
|
||||
"@codemirror/search": "6.5.2",
|
||||
"@codemirror/state": "6.2.1",
|
||||
"@codemirror/view": "6.16.0",
|
||||
"@codemirror/view": "6.17.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.10.0",
|
||||
"@formatjs/intl-displaynames": "6.5.0",
|
||||
@@ -50,10 +50,10 @@
|
||||
"@fullcalendar/luxon3": "6.1.8",
|
||||
"@fullcalendar/timegrid": "6.1.8",
|
||||
"@lezer/highlight": "1.1.6",
|
||||
"@lit-labs/context": "0.3.3",
|
||||
"@lit-labs/motion": "1.0.3",
|
||||
"@lit-labs/virtualizer": "2.0.4",
|
||||
"@lrnwebcomponents/simple-tooltip": "7.0.11",
|
||||
"@lit-labs/context": "0.4.0",
|
||||
"@lit-labs/motion": "1.0.4",
|
||||
"@lit-labs/virtualizer": "2.0.7",
|
||||
"@lrnwebcomponents/simple-tooltip": "7.0.16",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-button": "0.27.0",
|
||||
@@ -79,10 +79,9 @@
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "=1.0.0-pre.15",
|
||||
"@material/web": "=1.0.0-pre.16",
|
||||
"@mdi/js": "7.2.96",
|
||||
"@mdi/svg": "7.2.96",
|
||||
"@polymer/app-layout": "3.1.0",
|
||||
"@polymer/iron-flex-layout": "3.0.1",
|
||||
"@polymer/iron-input": "3.0.1",
|
||||
"@polymer/iron-resizable-behavior": "3.0.1",
|
||||
@@ -94,8 +93,8 @@
|
||||
"@polymer/paper-toast": "3.0.1",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.1.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.1.5",
|
||||
"@vaadin/combo-box": "24.1.6",
|
||||
"@vaadin/vaadin-themable-mixin": "24.1.6",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -103,17 +102,17 @@
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"chart.js": "3.3.2",
|
||||
"chart.js": "4.3.3",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.32.0",
|
||||
"cropperjs": "1.5.13",
|
||||
"core-js": "3.32.1",
|
||||
"cropperjs": "1.6.0",
|
||||
"date-fns": "2.30.0",
|
||||
"date-fns-tz": "2.0.0",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"fuse.js": "6.6.2",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"hls.js": "1.4.10",
|
||||
"hls.js": "1.4.12",
|
||||
"home-assistant-js-websocket": "8.2.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.5.0",
|
||||
@@ -121,8 +120,8 @@
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "1.0.4",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.3.0",
|
||||
"marked": "4.3.0",
|
||||
"luxon": "3.4.2",
|
||||
"marked": "7.0.5",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
@@ -133,10 +132,12 @@
|
||||
"roboto-fontface": "0.10.0",
|
||||
"rrule": "2.7.2",
|
||||
"sortablejs": "1.15.0",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "1.0.3",
|
||||
"tinykeys": "2.1.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "1.0.35",
|
||||
"unfetch": "5.0.0",
|
||||
"vis-data": "7.1.6",
|
||||
"vis-network": "9.1.6",
|
||||
@@ -152,12 +153,13 @@
|
||||
"xss": "1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.22.10",
|
||||
"@babel/core": "7.22.11",
|
||||
"@babel/plugin-proposal-decorators": "7.22.10",
|
||||
"@babel/plugin-transform-runtime": "7.22.10",
|
||||
"@babel/preset-env": "7.22.10",
|
||||
"@babel/preset-typescript": "7.22.5",
|
||||
"@babel/preset-env": "7.22.14",
|
||||
"@babel/preset-typescript": "7.22.11",
|
||||
"@koa/cors": "4.0.0",
|
||||
"@lokalise/node-api": "11.0.1",
|
||||
"@octokit/auth-oauth-device": "6.0.0",
|
||||
"@octokit/plugin-retry": "6.0.0",
|
||||
"@octokit/rest": "20.0.1",
|
||||
@@ -165,7 +167,7 @@
|
||||
"@rollup/plugin-babel": "6.0.3",
|
||||
"@rollup/plugin-commonjs": "25.0.4",
|
||||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/plugin-node-resolve": "15.1.0",
|
||||
"@rollup/plugin-node-resolve": "15.2.1",
|
||||
"@rollup/plugin-replace": "5.0.2",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.2",
|
||||
"@types/chromecast-caf-receiver": "6.0.9",
|
||||
@@ -174,39 +176,39 @@
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.0",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/leaflet": "1.9.3",
|
||||
"@types/leaflet-draw": "1.0.7",
|
||||
"@types/luxon": "3.3.1",
|
||||
"@types/marked": "4.3.1",
|
||||
"@types/leaflet": "1.9.4",
|
||||
"@types/leaflet-draw": "1.0.8",
|
||||
"@types/luxon": "3.3.2",
|
||||
"@types/mocha": "10.0.1",
|
||||
"@types/qrcode": "1.5.1",
|
||||
"@types/qrcode": "1.5.2",
|
||||
"@types/serve-handler": "6.1.1",
|
||||
"@types/sortablejs": "1.15.1",
|
||||
"@types/sortablejs": "1.15.2",
|
||||
"@types/tar": "6.1.5",
|
||||
"@types/ua-parser-js": "0.7.37",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "6.3.0",
|
||||
"@typescript-eslint/parser": "6.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||
"@typescript-eslint/parser": "6.5.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"chai": "4.3.7",
|
||||
"del": "7.0.0",
|
||||
"eslint": "8.47.0",
|
||||
"chai": "4.3.8",
|
||||
"del": "7.1.0",
|
||||
"eslint": "8.48.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "17.1.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint-import-resolver-webpack": "0.13.4",
|
||||
"eslint-import-resolver-webpack": "0.13.7",
|
||||
"eslint-plugin-disable": "2.0.3",
|
||||
"eslint-plugin-import": "2.28.0",
|
||||
"eslint-plugin-lit": "1.8.3",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
"eslint-plugin-lit": "1.9.1",
|
||||
"eslint-plugin-lit-a11y": "3.0.0",
|
||||
"eslint-plugin-unused-imports": "3.0.0",
|
||||
"eslint-plugin-wc": "1.5.0",
|
||||
"esprima": "4.0.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"glob": "10.3.3",
|
||||
"glob": "10.3.4",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-flatmap": "1.0.2",
|
||||
"gulp-json-transform": "0.4.8",
|
||||
@@ -217,16 +219,16 @@
|
||||
"husky": "8.0.3",
|
||||
"instant-mocha": "1.5.2",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "13.2.3",
|
||||
"lint-staged": "14.0.1",
|
||||
"lit-analyzer": "2.0.0-pre.3",
|
||||
"lodash.template": "4.5.0",
|
||||
"magic-string": "0.30.2",
|
||||
"magic-string": "0.30.3",
|
||||
"map-stream": "0.0.7",
|
||||
"mocha": "10.2.0",
|
||||
"object-hash": "3.0.0",
|
||||
"open": "9.1.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.0.1",
|
||||
"prettier": "3.0.3",
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
@@ -234,11 +236,11 @@
|
||||
"serve-handler": "6.1.5",
|
||||
"sinon": "15.2.0",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.14.1",
|
||||
"systemjs": "6.14.2",
|
||||
"tar": "6.1.15",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"ts-lit-plugin": "2.0.0-pre.1",
|
||||
"typescript": "5.1.6",
|
||||
"typescript": "5.2.2",
|
||||
"vinyl-buffer": "1.0.1",
|
||||
"vinyl-source-stream": "2.0.0",
|
||||
"webpack": "5.88.2",
|
||||
@@ -255,5 +257,5 @@
|
||||
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch",
|
||||
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||
},
|
||||
"packageManager": "yarn@3.6.1"
|
||||
"packageManager": "yarn@3.6.3"
|
||||
}
|
||||
|
BIN
public/static/images/logo_discord.png
Normal file
BIN
public/static/images/logo_discord.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
public/static/images/logo_twitter.png
Normal file
BIN
public/static/images/logo_twitter.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20230802.0"
|
||||
version = "20230906.1"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -15,7 +15,7 @@
|
||||
"lockFileMaintenance": {
|
||||
"description": ["Run after patch releases but before next beta"],
|
||||
"enabled": true,
|
||||
"schedule": ["on the 19th day of the month"]
|
||||
"schedule": ["on the 19th day of the month before 4am"]
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
|
@@ -8,40 +8,4 @@ set -eu -o pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ -z "${LOKALISE_TOKEN-}" ] && [ ! -f .lokalise_token ] ; then
|
||||
echo "Lokalise API token is required to download the latest set of" \
|
||||
"translations. Please create an account by using the following link:" \
|
||||
"https://lokalise.co/signup/3420425759f6d6d241f598.13594006/all/" \
|
||||
"Place your token in a new file \".lokalise_token\" in the repo" \
|
||||
"root directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load token from file if not already in the environment
|
||||
[ -z "${LOKALISE_TOKEN-}" ] && LOKALISE_TOKEN="$(<.lokalise_token)"
|
||||
|
||||
declare -A PROJECT_ID=( \
|
||||
[frontend]="3420425759f6d6d241f598.13594006" \
|
||||
[backend]="130246255a974bd3b5e8a1.51616605" \
|
||||
)
|
||||
|
||||
for project in ${!PROJECT_ID[*]}; do
|
||||
LOCAL_DIR=`pwd`/translations/${project}
|
||||
rm -f ${LOCAL_DIR}/* || mkdir -p ${LOCAL_DIR}
|
||||
docker run \
|
||||
-v ${LOCAL_DIR}:/opt/dest/locale \
|
||||
--rm \
|
||||
lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d \
|
||||
lokalise2 \
|
||||
--token ${LOKALISE_TOKEN} \
|
||||
--project-id ${PROJECT_ID[${project}]} \
|
||||
file download \
|
||||
--export-empty-as skip \
|
||||
--format json \
|
||||
--json-unescaped-slashes=true \
|
||||
--replace-breaks=false \
|
||||
--original-filenames=false \
|
||||
--unzip-to /opt/dest
|
||||
done
|
||||
|
||||
./node_modules/.bin/gulp check-downloaded-translations
|
||||
./node_modules/.bin/gulp download-translations
|
@@ -1,3 +1,4 @@
|
||||
import punycode from "punycode";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import punycode from "punycode";
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import "../components/ha-alert";
|
||||
@@ -35,6 +35,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
|
||||
@property() public oauth2State?: string;
|
||||
|
||||
@property() public translationFragment = "page-authorize";
|
||||
|
||||
@state() private _authProvider?: AuthProvider;
|
||||
|
||||
@state() private _authProviders?: AuthProvider[];
|
||||
@@ -45,7 +47,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.translationFragment = "page-authorize";
|
||||
const query = extractSearchParamsObject() as AuthUrlSearchParams;
|
||||
if (query.client_id) {
|
||||
this.clientId = query.client_id;
|
||||
@@ -102,7 +103,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
: nothing}
|
||||
|
||||
<ha-auth-flow
|
||||
.resources=${this.resources}
|
||||
.clientId=${this.clientId}
|
||||
.redirectUri=${this.redirectUri}
|
||||
.oauth2State=${this.oauth2State}
|
||||
|
@@ -107,6 +107,7 @@ export class HaPasswordManagerPolyfill extends LitElement {
|
||||
.value=${this.stepData[schema.name] || ""}
|
||||
.autocomplete=${schema.autocomplete}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { clamp } from "../number/clamp";
|
||||
|
||||
const DEFAULT_MIN_KELVIN = 2700;
|
||||
const DEFAULT_MAX_KELVIN = 6500;
|
||||
export const DEFAULT_MIN_KELVIN = 2700;
|
||||
export const DEFAULT_MAX_KELVIN = 6500;
|
||||
|
||||
export const temperature2rgb = (
|
||||
temperature: number
|
||||
|
@@ -49,6 +49,7 @@ import {
|
||||
mdiProgressClock,
|
||||
mdiRayVertex,
|
||||
mdiRemote,
|
||||
mdiRobotMower,
|
||||
mdiRobotVacuum,
|
||||
mdiScriptText,
|
||||
mdiSineWave,
|
||||
@@ -99,6 +100,7 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
input_number: mdiRayVertex,
|
||||
input_select: mdiFormatListBulleted,
|
||||
input_text: mdiFormTextbox,
|
||||
lawn_mower: mdiRobotMower,
|
||||
light: mdiLightbulb,
|
||||
mailbox: mdiMailbox,
|
||||
notify: mdiCommentAlert,
|
||||
@@ -187,6 +189,7 @@ export const DOMAINS_WITH_CARD = [
|
||||
"input_number",
|
||||
"input_text",
|
||||
"humidifier",
|
||||
"lawn_mower",
|
||||
"lock",
|
||||
"media_player",
|
||||
"number",
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
DOMAIN_ATTRIBUTES_UNITS,
|
||||
TEMPERATURE_ATTRIBUTES,
|
||||
} from "../../data/entity_attributes";
|
||||
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { WeatherEntity, getWeatherUnit } from "../../data/weather";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import checkValidDate from "../datetime/check_valid_date";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
@@ -9,8 +14,10 @@ import { formatNumber } from "../number/format_number";
|
||||
import { capitalizeFirstLetter } from "../string/capitalize-first-letter";
|
||||
import { isDate } from "../string/is_date";
|
||||
import { isTimestamp } from "../string/is_timestamp";
|
||||
import { blankBeforePercent } from "../translations/blank_before_percent";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
|
||||
export const computeAttributeValueDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
@@ -31,7 +38,40 @@ export const computeAttributeValueDisplay = (
|
||||
|
||||
// Number value, return formatted number
|
||||
if (typeof attributeValue === "number") {
|
||||
return formatNumber(attributeValue, locale);
|
||||
const formattedValue = formatNumber(attributeValue, locale);
|
||||
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
let unit = DOMAIN_ATTRIBUTES_UNITS[domain]?.[attribute] as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
if (domain === "light" && attribute === "brightness") {
|
||||
const percentage = Math.round((attributeValue / 255) * 100);
|
||||
return `${percentage}${blankBeforePercent(locale)}%`;
|
||||
}
|
||||
|
||||
if (domain === "weather") {
|
||||
unit = getWeatherUnit(config, stateObj as WeatherEntity, attribute);
|
||||
}
|
||||
|
||||
if (unit === "%") {
|
||||
return `${formattedValue}${blankBeforePercent(locale)}${unit}`;
|
||||
}
|
||||
|
||||
if (unit === "°") {
|
||||
return `${formattedValue}${unit}`;
|
||||
}
|
||||
|
||||
if (unit) {
|
||||
return `${formattedValue} ${unit}`;
|
||||
}
|
||||
|
||||
if (TEMPERATURE_ATTRIBUTES.has(attribute)) {
|
||||
return `${formattedValue} ${config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
return formattedValue;
|
||||
}
|
||||
|
||||
// Special handling in case this is a string with an known format
|
||||
|
@@ -26,6 +26,7 @@ export const FIXED_DOMAIN_STATES = {
|
||||
humidifier: ["on", "off"],
|
||||
input_boolean: ["on", "off"],
|
||||
input_button: [],
|
||||
lawn_mower: ["error", "paused", "mowing", "docked"],
|
||||
light: ["on", "off"],
|
||||
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
|
||||
media_player: [
|
||||
|
@@ -34,6 +34,8 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
return compareState !== "not_home";
|
||||
case "lawn_mower":
|
||||
return ["mowing", "error"].includes(compareState);
|
||||
case "lock":
|
||||
return compareState !== "locked";
|
||||
case "media_player":
|
||||
|
@@ -22,6 +22,7 @@ const STATE_COLORED_DOMAIN = new Set([
|
||||
"group",
|
||||
"humidifier",
|
||||
"input_boolean",
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
|
@@ -4,7 +4,7 @@ export const clamp = (value: number, min: number, max: number) =>
|
||||
// Variant that only applies the clamping to a border if the border is defined
|
||||
export const conditionalClamp = (value: number, min?: number, max?: number) => {
|
||||
let result: number;
|
||||
result = min ? Math.max(value, min) : value;
|
||||
result = max ? Math.min(result, max) : result;
|
||||
result = min != null ? Math.max(value, min) : value;
|
||||
result = max != null ? Math.min(result, max) : result;
|
||||
return result;
|
||||
};
|
||||
|
@@ -108,7 +108,7 @@ export const formatNumber = (
|
||||
* @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined`
|
||||
*/
|
||||
export const getNumberFormatOptions = (
|
||||
entityState: HassEntity,
|
||||
entityState?: HassEntity,
|
||||
entity?: EntityRegistryDisplayEntry
|
||||
): Intl.NumberFormatOptions | undefined => {
|
||||
const precision = entity?.display_precision;
|
||||
@@ -119,8 +119,8 @@ export const getNumberFormatOptions = (
|
||||
};
|
||||
}
|
||||
if (
|
||||
Number.isInteger(Number(entityState.attributes?.step)) &&
|
||||
Number.isInteger(Number(entityState.state))
|
||||
Number.isInteger(Number(entityState?.attributes?.step)) &&
|
||||
Number.isInteger(Number(entityState?.state))
|
||||
) {
|
||||
return { maximumFractionDigits: 0 };
|
||||
}
|
||||
|
@@ -3,25 +3,30 @@ import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LocalizeFunc } from "./localize";
|
||||
|
||||
export type FormatEntityStateFunc = {
|
||||
formatEntityState: (stateObj: HassEntity, state?: string) => string;
|
||||
formatEntityAttributeValue: (
|
||||
stateObj: HassEntity,
|
||||
attribute: string,
|
||||
value?: any
|
||||
) => string;
|
||||
formatEntityAttributeName: (
|
||||
stateObj: HassEntity,
|
||||
attribute: string
|
||||
) => string;
|
||||
};
|
||||
export type FormatEntityStateFunc = (
|
||||
stateObj: HassEntity,
|
||||
state?: string
|
||||
) => string;
|
||||
export type FormatEntityAttributeValueFunc = (
|
||||
stateObj: HassEntity,
|
||||
attribute: string,
|
||||
value?: any
|
||||
) => string;
|
||||
export type formatEntityAttributeNameFunc = (
|
||||
stateObj: HassEntity,
|
||||
attribute: string
|
||||
) => string;
|
||||
|
||||
export const computeFormatFunctions = async (
|
||||
localize: LocalizeFunc,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
entities: HomeAssistant["entities"]
|
||||
): Promise<FormatEntityStateFunc> => {
|
||||
): Promise<{
|
||||
formatEntityState: FormatEntityStateFunc;
|
||||
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
|
||||
formatEntityAttributeName: formatEntityAttributeNameFunc;
|
||||
}> => {
|
||||
const { computeStateDisplay } = await import(
|
||||
"../entity/compute_state_display"
|
||||
);
|
||||
|
@@ -11,23 +11,18 @@ export type LocalizeKeys =
|
||||
| `ui.card.alarm_control_panel.${string}`
|
||||
| `ui.card.weather.attributes.${string}`
|
||||
| `ui.card.weather.cardinal_direction.${string}`
|
||||
| `ui.card.lawn_mower.actions.${string}`
|
||||
| `ui.components.calendar.event.rrule.${string}`
|
||||
| `ui.components.logbook.${string}`
|
||||
| `ui.components.selectors.file.${string}`
|
||||
| `ui.dialogs.entity_registry.editor.${string}`
|
||||
| `ui.dialogs.more_info_control.lawn_mower.${string}`
|
||||
| `ui.dialogs.more_info_control.vacuum.${string}`
|
||||
| `ui.dialogs.quick-bar.commands.${string}`
|
||||
| `ui.dialogs.unhealthy.reason.${string}`
|
||||
| `ui.dialogs.unsupported.reason.${string}`
|
||||
| `ui.panel.config.${string}.${"caption" | "description"}`
|
||||
| `ui.panel.config.automation.${string}`
|
||||
| `ui.panel.config.dashboard.${string}`
|
||||
| `ui.panel.config.devices.${string}`
|
||||
| `ui.panel.config.energy.${string}`
|
||||
| `ui.panel.config.info.${string}`
|
||||
| `ui.panel.config.lovelace.${string}`
|
||||
| `ui.panel.config.network.${string}`
|
||||
| `ui.panel.config.scene.${string}`
|
||||
| `ui.panel.config.zha.${string}`
|
||||
| `ui.panel.config.zwave_js.${string}`
|
||||
| `ui.panel.lovelace.card.${string}`
|
||||
|
@@ -1,100 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { EventsMixin } from "../../mixins/events-mixin";
|
||||
import "./ha-progress-button";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaCallServiceButton extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<ha-progress-button
|
||||
id="progress"
|
||||
progress="[[progress]]"
|
||||
disabled="[[disabled]]"
|
||||
on-click="buttonTapped"
|
||||
tabindex="0"
|
||||
><slot></slot
|
||||
></ha-progress-button>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
progress: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
domain: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
service: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
serviceData: {
|
||||
type: Object,
|
||||
value: {},
|
||||
},
|
||||
|
||||
confirmation: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
callService() {
|
||||
this.progress = true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const el = this;
|
||||
const eventData = {
|
||||
domain: this.domain,
|
||||
service: this.service,
|
||||
serviceData: this.serviceData,
|
||||
};
|
||||
|
||||
this.hass
|
||||
.callService(this.domain, this.service, this.serviceData)
|
||||
.then(
|
||||
() => {
|
||||
el.progress = false;
|
||||
el.$.progress.actionSuccess();
|
||||
eventData.success = true;
|
||||
},
|
||||
() => {
|
||||
el.progress = false;
|
||||
el.$.progress.actionError();
|
||||
eventData.success = false;
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
el.fire("hass-service-called", eventData);
|
||||
});
|
||||
}
|
||||
|
||||
buttonTapped() {
|
||||
if (this.confirmation) {
|
||||
showConfirmationDialog(this, {
|
||||
text: this.confirmation,
|
||||
confirm: () => this.callService(),
|
||||
});
|
||||
} else {
|
||||
this.callService();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-call-service-button", HaCallServiceButton);
|
92
src/components/buttons/ha-call-service-button.ts
Normal file
92
src/components/buttons/ha-call-service-button.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { LitElement, TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import "./ha-progress-button";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-call-service-button")
|
||||
class HaCallServiceButton extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public progress = false;
|
||||
|
||||
@property() public domain!: string;
|
||||
|
||||
@property() public service!: string;
|
||||
|
||||
@property({ type: Object }) public serviceData = {};
|
||||
|
||||
@property() public confirmation?;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<ha-progress-button
|
||||
.progress=${this.progress}
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._buttonTapped}
|
||||
tabindex="0"
|
||||
>
|
||||
<slot></slot
|
||||
></ha-progress-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _callService() {
|
||||
this.progress = true;
|
||||
const eventData = {
|
||||
domain: this.domain,
|
||||
service: this.service,
|
||||
serviceData: this.serviceData,
|
||||
success: false,
|
||||
};
|
||||
|
||||
const progressElement =
|
||||
this.shadowRoot!.querySelector("ha-progress-button")!;
|
||||
|
||||
try {
|
||||
await this.hass.callService(this.domain, this.service, this.serviceData);
|
||||
this.progress = false;
|
||||
progressElement.actionSuccess();
|
||||
eventData.success = true;
|
||||
} catch (e) {
|
||||
this.progress = false;
|
||||
progressElement.actionError();
|
||||
eventData.success = false;
|
||||
return;
|
||||
} finally {
|
||||
fireEvent(this, "hass-service-called", eventData);
|
||||
}
|
||||
}
|
||||
|
||||
private _buttonTapped() {
|
||||
if (this.confirmation) {
|
||||
showConfirmationDialog(this, {
|
||||
text: this.confirmation,
|
||||
confirm: () => this._callService(),
|
||||
});
|
||||
} else {
|
||||
this._callService();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-call-service-button": HaCallServiceButton;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"hass-service-called": {
|
||||
domain: string;
|
||||
service: string;
|
||||
serviceData: object;
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
}
|
@@ -15,13 +15,20 @@ import { HomeAssistant } from "../../types";
|
||||
|
||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
|
||||
interface Tooltip extends TooltipModel<any> {
|
||||
export interface ChartResizeOptions {
|
||||
aspectRatio?: number;
|
||||
height?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface Tooltip
|
||||
extends Omit<TooltipModel<any>, "tooltipPosition" | "hasValue" | "getProps"> {
|
||||
top: string;
|
||||
left: string;
|
||||
}
|
||||
|
||||
@customElement("ha-chart-base")
|
||||
export default class HaChartBase extends LitElement {
|
||||
export class HaChartBase extends LitElement {
|
||||
public chart?: Chart;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -45,14 +52,6 @@ export default class HaChartBase extends LitElement {
|
||||
|
||||
@state() private _hiddenDatasets: Set<number> = new Set();
|
||||
|
||||
private _releaseCanvas() {
|
||||
// release the canvas memory to prevent
|
||||
// safari from running out of memory.
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
this._releaseCanvas();
|
||||
super.disconnectedCallback();
|
||||
@@ -65,6 +64,36 @@ export default class HaChartBase extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public updateChart = (
|
||||
mode:
|
||||
| "resize"
|
||||
| "reset"
|
||||
| "none"
|
||||
| "hide"
|
||||
| "show"
|
||||
| "default"
|
||||
| "active"
|
||||
| undefined
|
||||
): void => {
|
||||
this.chart?.update(mode);
|
||||
};
|
||||
|
||||
public resize = (options?: ChartResizeOptions): void => {
|
||||
if (options?.aspectRatio && !options.height) {
|
||||
options.height = Math.round(
|
||||
(options.width ?? this.clientWidth) / options.aspectRatio
|
||||
);
|
||||
} else if (options?.aspectRatio && !options.width) {
|
||||
options.width = Math.round(
|
||||
(options.height ?? this.clientHeight) * options.aspectRatio
|
||||
);
|
||||
}
|
||||
this.chart?.resize(
|
||||
options?.width ?? this.clientWidth,
|
||||
options?.height ?? this.clientHeight
|
||||
);
|
||||
};
|
||||
|
||||
protected firstUpdated() {
|
||||
this._setupChart();
|
||||
this.data.datasets.forEach((dataset, index) => {
|
||||
@@ -80,14 +109,11 @@ export default class HaChartBase extends LitElement {
|
||||
if (!this.hasUpdated || !this.chart) {
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("plugins")) {
|
||||
if (changedProps.has("plugins") || changedProps.has("chartType")) {
|
||||
this.chart.destroy();
|
||||
this._setupChart();
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("chartType")) {
|
||||
this.chart.config.type = this.chartType;
|
||||
}
|
||||
if (changedProps.has("data")) {
|
||||
if (this._hiddenDatasets.size) {
|
||||
this.data.datasets.forEach((dataset, index) => {
|
||||
@@ -131,55 +157,70 @@ export default class HaChartBase extends LitElement {
|
||||
</div>`
|
||||
: ""}
|
||||
<div
|
||||
class="chartContainer"
|
||||
class="animationContainer"
|
||||
style=${styleMap({
|
||||
height: `${this.height ?? this._chartHeight}px`,
|
||||
height: `${this.height || this._chartHeight || 0}px`,
|
||||
overflow: this._chartHeight ? "initial" : "hidden",
|
||||
"padding-left": `${computeRTL(this.hass) ? 0 : this.paddingYAxis}px`,
|
||||
"padding-right": `${computeRTL(this.hass) ? this.paddingYAxis : 0}px`,
|
||||
})}
|
||||
>
|
||||
<canvas></canvas>
|
||||
${this._tooltip
|
||||
? html`<div
|
||||
class="chartTooltip ${classMap({ [this._tooltip.yAlign]: true })}"
|
||||
style=${styleMap({
|
||||
top: this._tooltip.top,
|
||||
left: this._tooltip.left,
|
||||
})}
|
||||
>
|
||||
<div class="title">${this._tooltip.title}</div>
|
||||
${this._tooltip.beforeBody
|
||||
? html`<div class="beforeBody">
|
||||
${this._tooltip.beforeBody}
|
||||
</div>`
|
||||
: ""}
|
||||
<div>
|
||||
<ul>
|
||||
${this._tooltip.body.map(
|
||||
(item, i) =>
|
||||
html`<li>
|
||||
<div
|
||||
class="bullet"
|
||||
style=${styleMap({
|
||||
backgroundColor: this._tooltip!.labelColors[i]
|
||||
.backgroundColor as string,
|
||||
borderColor: this._tooltip!.labelColors[i]
|
||||
.borderColor as string,
|
||||
})}
|
||||
></div>
|
||||
${item.lines.join("\n")}
|
||||
</li>`
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
${this._tooltip.footer.length
|
||||
? html`<div class="footer">
|
||||
${this._tooltip.footer.map((item) => html`${item}<br />`)}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>`
|
||||
: ""}
|
||||
<div
|
||||
class="chartContainer"
|
||||
style=${styleMap({
|
||||
height: `${
|
||||
this.height ?? this._chartHeight ?? this.clientWidth / 2
|
||||
}px`,
|
||||
"padding-left": `${
|
||||
computeRTL(this.hass) ? 0 : this.paddingYAxis
|
||||
}px`,
|
||||
"padding-right": `${
|
||||
computeRTL(this.hass) ? this.paddingYAxis : 0
|
||||
}px`,
|
||||
})}
|
||||
>
|
||||
<canvas></canvas>
|
||||
${this._tooltip
|
||||
? html`<div
|
||||
class="chartTooltip ${classMap({
|
||||
[this._tooltip.yAlign]: true,
|
||||
})}"
|
||||
style=${styleMap({
|
||||
top: this._tooltip.top,
|
||||
left: this._tooltip.left,
|
||||
})}
|
||||
>
|
||||
<div class="title">${this._tooltip.title}</div>
|
||||
${this._tooltip.beforeBody
|
||||
? html`<div class="beforeBody">
|
||||
${this._tooltip.beforeBody}
|
||||
</div>`
|
||||
: ""}
|
||||
<div>
|
||||
<ul>
|
||||
${this._tooltip.body.map(
|
||||
(item, i) =>
|
||||
html`<li>
|
||||
<div
|
||||
class="bullet"
|
||||
style=${styleMap({
|
||||
backgroundColor: this._tooltip!.labelColors[i]
|
||||
.backgroundColor as string,
|
||||
borderColor: this._tooltip!.labelColors[i]
|
||||
.borderColor as string,
|
||||
})}
|
||||
></div>
|
||||
${item.lines.join("\n")}
|
||||
</li>`
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
${this._tooltip.footer.length
|
||||
? html`<div class="footer">
|
||||
${this._tooltip.footer.map((item) => html`${item}<br />`)}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -213,6 +254,7 @@ export default class HaChartBase extends LitElement {
|
||||
|
||||
private _createOptions() {
|
||||
return {
|
||||
maintainAspectRatio: false,
|
||||
...this.options,
|
||||
plugins: {
|
||||
...this.options?.plugins,
|
||||
@@ -233,10 +275,10 @@ export default class HaChartBase extends LitElement {
|
||||
return [
|
||||
...(this.plugins || []),
|
||||
{
|
||||
id: "afterRenderHook",
|
||||
afterRender: (chart) => {
|
||||
id: "resizeHook",
|
||||
resize: (chart) => {
|
||||
const change = chart.height - (this._chartHeight ?? 0);
|
||||
if (!this._chartHeight || change > 0 || change < -12) {
|
||||
if (!this._chartHeight || change > 12 || change < -12) {
|
||||
// hysteresis to prevent infinite render loops
|
||||
this._chartHeight = chart.height;
|
||||
}
|
||||
@@ -288,21 +330,13 @@ export default class HaChartBase extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
public updateChart = (
|
||||
mode:
|
||||
| "resize"
|
||||
| "reset"
|
||||
| "none"
|
||||
| "hide"
|
||||
| "show"
|
||||
| "normal"
|
||||
| "active"
|
||||
| undefined
|
||||
): void => {
|
||||
private _releaseCanvas() {
|
||||
// release the canvas memory to prevent
|
||||
// safari from running out of memory.
|
||||
if (this.chart) {
|
||||
this.chart.update(mode);
|
||||
this.chart.destroy();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
@@ -310,11 +344,14 @@ export default class HaChartBase extends LitElement {
|
||||
display: block;
|
||||
position: var(--chart-base-position, relative);
|
||||
}
|
||||
.chartContainer {
|
||||
.animationContainer {
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.chartContainer {
|
||||
position: relative;
|
||||
}
|
||||
canvas {
|
||||
max-height: var(--chart-max-height, 400px);
|
||||
}
|
||||
|
@@ -1,23 +1,28 @@
|
||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
getNumberFormatOptions,
|
||||
} from "../../common/number/format_number";
|
||||
import { LineChartEntity, LineChartState } from "../../data/history";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import {
|
||||
ChartResizeOptions,
|
||||
HaChartBase,
|
||||
MIN_TIME_BETWEEN_UPDATES,
|
||||
} from "./ha-chart-base";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const parsed = parseFloat(value);
|
||||
return isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
class StateHistoryChartLine extends LitElement {
|
||||
export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data: LineChartEntity[] = [];
|
||||
@@ -46,6 +51,12 @@ class StateHistoryChartLine extends LitElement {
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
@query("ha-chart-base") private _chart?: HaChartBase;
|
||||
|
||||
public resize = (options?: ChartResizeOptions): void => {
|
||||
this._chart?.resize(options);
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-chart-base
|
||||
@@ -125,7 +136,17 @@ class StateHistoryChartLine extends LitElement {
|
||||
label: (context) =>
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
context.parsed.y,
|
||||
this.hass.locale
|
||||
this.hass.locale,
|
||||
this.data[context.datasetIndex]?.entity_id
|
||||
? getNumberFormatOptions(
|
||||
this.hass.states[
|
||||
this.data[context.datasetIndex].entity_id
|
||||
],
|
||||
this.hass.entities[
|
||||
this.data[context.datasetIndex].entity_id
|
||||
]
|
||||
)
|
||||
: undefined
|
||||
)} ${this.unit}`,
|
||||
},
|
||||
},
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -8,7 +8,11 @@ import { numberFormatToLocale } from "../../common/number/format_number";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { TimelineEntity } from "../../data/history";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import {
|
||||
ChartResizeOptions,
|
||||
HaChartBase,
|
||||
MIN_TIME_BETWEEN_UPDATES,
|
||||
} from "./ha-chart-base";
|
||||
import type { TimeLineData } from "./timeline-chart/const";
|
||||
import { computeTimelineColor } from "./timeline-chart/timeline-color";
|
||||
|
||||
@@ -46,6 +50,12 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
@query("ha-chart-base") private _chart?: HaChartBase;
|
||||
|
||||
public resize = (options?: ChartResizeOptions): void => {
|
||||
this._chart?.resize(options);
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-chart-base
|
||||
|
@@ -6,7 +6,13 @@ import {
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, eventOptions, property, state } from "lit/decorators";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
queryAll,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import {
|
||||
@@ -18,6 +24,9 @@ import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./state-history-chart-line";
|
||||
import "./state-history-chart-timeline";
|
||||
import type { StateHistoryChartLine } from "./state-history-chart-line";
|
||||
import type { StateHistoryChartTimeline } from "./state-history-chart-timeline";
|
||||
import { ChartResizeOptions } from "./ha-chart-base";
|
||||
|
||||
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
|
||||
|
||||
@@ -75,6 +84,16 @@ export class StateHistoryCharts extends LitElement {
|
||||
// @ts-ignore
|
||||
@restoreScroll(".container") private _savedScrollPos?: number;
|
||||
|
||||
@queryAll("state-history-chart-line")
|
||||
private _charts?: StateHistoryChartLine[];
|
||||
|
||||
public resize = (options?: ChartResizeOptions): void => {
|
||||
this._charts?.forEach(
|
||||
(chart: StateHistoryChartLine | StateHistoryChartTimeline) =>
|
||||
chart.resize(options)
|
||||
);
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!isComponentLoaded(this.hass, "history")) {
|
||||
return html`<div class="info">
|
||||
|
@@ -12,7 +12,7 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from "../../data/recorder";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
import type { ChartResizeOptions, HaChartBase } from "./ha-chart-base";
|
||||
|
||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||
mean: "mean",
|
||||
@@ -42,7 +43,7 @@ export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||
};
|
||||
|
||||
@customElement("statistics-chart")
|
||||
class StatisticsChart extends LitElement {
|
||||
export class StatisticsChart extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public statisticsData?: Statistics;
|
||||
@@ -75,8 +76,14 @@ class StatisticsChart extends LitElement {
|
||||
|
||||
@state() private _chartOptions?: ChartOptions;
|
||||
|
||||
@query("ha-chart-base") private _chart?: HaChartBase;
|
||||
|
||||
private _computedStyle?: CSSStyleDeclaration;
|
||||
|
||||
public resize = (options?: ChartResizeOptions): void => {
|
||||
this._chart?.resize(options);
|
||||
};
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return changedProps.size > 1 || !changedProps.has("hass");
|
||||
}
|
||||
|
@@ -1,3 +1,8 @@
|
||||
import type {
|
||||
BarControllerChartOptions,
|
||||
BarControllerDatasetOptions,
|
||||
} from "chart.js";
|
||||
|
||||
export interface TimeLineData {
|
||||
start: Date;
|
||||
end: Date;
|
||||
|
@@ -16,7 +16,7 @@ export interface TextBaroptions extends BarOptions {
|
||||
export class TextBarElement extends BarElement {
|
||||
static id = "textbar";
|
||||
|
||||
draw(ctx) {
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
super.draw(ctx);
|
||||
const options = this.options as TextBaroptions;
|
||||
const { x, y, base, width, text } = (
|
||||
|
@@ -2,6 +2,95 @@ import { BarController, BarElement } from "chart.js";
|
||||
import { TimeLineData } from "./const";
|
||||
import { TextBarProps } from "./textbar-element";
|
||||
|
||||
function borderProps(properties) {
|
||||
let reverse;
|
||||
let start;
|
||||
let end;
|
||||
let top;
|
||||
let bottom;
|
||||
if (properties.horizontal) {
|
||||
reverse = properties.base > properties.x;
|
||||
start = "left";
|
||||
end = "right";
|
||||
} else {
|
||||
reverse = properties.base < properties.y;
|
||||
start = "bottom";
|
||||
end = "top";
|
||||
}
|
||||
if (reverse) {
|
||||
top = "end";
|
||||
bottom = "start";
|
||||
} else {
|
||||
top = "start";
|
||||
bottom = "end";
|
||||
}
|
||||
return { start, end, reverse, top, bottom };
|
||||
}
|
||||
|
||||
function setBorderSkipped(properties, options, stack, index) {
|
||||
let edge = options.borderSkipped;
|
||||
const res = {};
|
||||
|
||||
if (!edge) {
|
||||
properties.borderSkipped = res;
|
||||
return;
|
||||
}
|
||||
|
||||
if (edge === true) {
|
||||
properties.borderSkipped = {
|
||||
top: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
left: true,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const { start, end, reverse, top, bottom } = borderProps(properties);
|
||||
|
||||
if (edge === "middle" && stack) {
|
||||
properties.enableBorderRadius = true;
|
||||
if ((stack._top || 0) === index) {
|
||||
edge = top;
|
||||
} else if ((stack._bottom || 0) === index) {
|
||||
edge = bottom;
|
||||
} else {
|
||||
res[parseEdge(bottom, start, end, reverse)] = true;
|
||||
edge = top;
|
||||
}
|
||||
}
|
||||
|
||||
res[parseEdge(edge, start, end, reverse)] = true;
|
||||
properties.borderSkipped = res;
|
||||
}
|
||||
|
||||
function parseEdge(edge, a, b, reverse) {
|
||||
if (reverse) {
|
||||
edge = swap(edge, a, b);
|
||||
edge = startEnd(edge, b, a);
|
||||
} else {
|
||||
edge = startEnd(edge, a, b);
|
||||
}
|
||||
return edge;
|
||||
}
|
||||
|
||||
function swap(orig, v1, v2) {
|
||||
return orig === v1 ? v2 : orig === v2 ? v1 : orig;
|
||||
}
|
||||
|
||||
function startEnd(v, start, end) {
|
||||
return v === "start" ? start : v === "end" ? end : v;
|
||||
}
|
||||
|
||||
function setInflateAmount(
|
||||
properties,
|
||||
{ inflateAmount }: { inflateAmount?: string | number },
|
||||
ratio
|
||||
) {
|
||||
properties.inflateAmount =
|
||||
inflateAmount === "auto" ? (ratio === 1 ? 0.33 : 0) : inflateAmount;
|
||||
}
|
||||
|
||||
function parseValue(entry, item, vScale, i) {
|
||||
const startValue = vScale.parse(entry.start, i);
|
||||
const endValue = vScale.parse(entry.end, i);
|
||||
@@ -97,7 +186,7 @@ export class TimelineController extends BarController {
|
||||
bars: BarElement[],
|
||||
start: number,
|
||||
count: number,
|
||||
mode: "reset" | "resize" | "none" | "hide" | "show" | "normal" | "active"
|
||||
mode: "reset" | "resize" | "none" | "hide" | "show" | "default" | "active"
|
||||
) {
|
||||
const vScale = this._cachedMeta.vScale!;
|
||||
const iScale = this._cachedMeta.iScale!;
|
||||
@@ -114,15 +203,15 @@ export class TimelineController extends BarController {
|
||||
for (let index = start; index < start + count; index++) {
|
||||
const data = dataset.data[index] as TimeLineData;
|
||||
|
||||
// @ts-ignore
|
||||
const y = vScale.getPixelForValue(this.index);
|
||||
|
||||
// @ts-ignore
|
||||
const xStart = iScale.getPixelForValue(data.start.getTime());
|
||||
// @ts-ignore
|
||||
const xEnd = iScale.getPixelForValue(data.end.getTime());
|
||||
const width = xEnd - xStart;
|
||||
|
||||
const parsed = this.getParsed(index);
|
||||
const stack = (parsed._stacks || {})[vScale.axis];
|
||||
|
||||
const height = 10;
|
||||
|
||||
const properties: TextBarProps = {
|
||||
@@ -145,7 +234,10 @@ export class TimelineController extends BarController {
|
||||
backgroundColor: data.color,
|
||||
};
|
||||
}
|
||||
const options = properties.options || bars[index].options;
|
||||
|
||||
setBorderSkipped(properties, options, stack, index);
|
||||
setInflateAmount(properties, options, 1);
|
||||
this.updateElement(bars[index], index, properties as any, mode);
|
||||
}
|
||||
}
|
||||
|
@@ -458,7 +458,8 @@ export class HaDataTable extends LitElement {
|
||||
filteredData,
|
||||
this._sortColumns[this._sortColumn],
|
||||
this._sortDirection,
|
||||
this._sortColumn
|
||||
this._sortColumn,
|
||||
this.hass.locale.language
|
||||
)
|
||||
: filteredData;
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// To use comlink under ES5
|
||||
import "proxy-polyfill";
|
||||
import { expose } from "comlink";
|
||||
import "proxy-polyfill";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import type {
|
||||
ClonedDataTableColumnData,
|
||||
DataTableRowData,
|
||||
@@ -39,7 +40,8 @@ const sortData = (
|
||||
data: DataTableRowData[],
|
||||
column: ClonedDataTableColumnData,
|
||||
direction: SortingDirection,
|
||||
sortColumn: string
|
||||
sortColumn: string,
|
||||
language?: string
|
||||
) =>
|
||||
data.sort((a, b) => {
|
||||
let sort = 1;
|
||||
@@ -58,13 +60,8 @@ const sortData = (
|
||||
if (column.type === "numeric") {
|
||||
valA = isNaN(valA) ? undefined : Number(valA);
|
||||
valB = isNaN(valB) ? undefined : Number(valB);
|
||||
} else {
|
||||
if (typeof valA === "string") {
|
||||
valA = valA.toUpperCase();
|
||||
}
|
||||
if (typeof valB === "string") {
|
||||
valB = valB.toUpperCase();
|
||||
}
|
||||
} else if (typeof valA === "string" && typeof valB === "string") {
|
||||
return sort * stringCompare(valA, valB, language);
|
||||
}
|
||||
|
||||
// Ensure "undefined" and "null" are always sorted to the bottom
|
||||
|
@@ -27,10 +27,12 @@ export const filterData = (
|
||||
filter: FilterDataParamTypes[2]
|
||||
): Promise<ReturnType<FilterDataType>> =>
|
||||
getWorker().filterData(data, columns, filter);
|
||||
|
||||
export const sortData = (
|
||||
data: SortDataParamTypes[0],
|
||||
columns: SortDataParamTypes[1],
|
||||
direction: SortDataParamTypes[2],
|
||||
sortColumn: SortDataParamTypes[3]
|
||||
sortColumn: SortDataParamTypes[3],
|
||||
language?: SortDataParamTypes[4]
|
||||
): Promise<ReturnType<SortDataType>> =>
|
||||
getWorker().sortData(data, columns, direction, sortColumn);
|
||||
getWorker().sortData(data, columns, direction, sortColumn, language);
|
||||
|
@@ -324,6 +324,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
.renderer=${rowRenderer}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
item-id-path="id"
|
||||
item-value-path="id"
|
||||
item-label-path="name"
|
||||
@opened-changed=${this._openedChanged}
|
||||
|
@@ -1,11 +1,9 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, nothing } from "lit";
|
||||
import { LitElement, PropertyValues, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeStateDisplay } from "../../common/entity/compute_state_display";
|
||||
import { getStates } from "../../common/entity/get_states";
|
||||
import { computeAttributeValueDisplay } from "../../common/entity/compute_attribute_display";
|
||||
import { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
|
||||
@@ -58,20 +56,9 @@ class HaEntityStatePicker extends LitElement {
|
||||
? getStates(state, this.attribute).map((key) => ({
|
||||
value: key,
|
||||
label: !this.attribute
|
||||
? computeStateDisplay(
|
||||
this.hass.localize,
|
||||
? this.hass.formatEntityState(state, key)
|
||||
: this.hass.formatEntityAttributeValue(
|
||||
state,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
key
|
||||
)
|
||||
: computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
state,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
this.attribute,
|
||||
key
|
||||
),
|
||||
|
@@ -12,7 +12,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
|
||||
import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||
import { computeStateDisplay } from "../../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
@@ -62,6 +61,8 @@ export class HaStateLabelBadge extends LitElement {
|
||||
|
||||
@property() public image?: string;
|
||||
|
||||
@property() public showName?: boolean;
|
||||
|
||||
@state() private _timerTimeRemaining?: number;
|
||||
|
||||
private _connected?: boolean;
|
||||
@@ -132,7 +133,9 @@ export class HaStateLabelBadge extends LitElement {
|
||||
entityState,
|
||||
this._timerTimeRemaining
|
||||
)}
|
||||
.description=${this.name ?? computeStateName(entityState)}
|
||||
.description=${this.showName === false
|
||||
? undefined
|
||||
: this.name ?? computeStateName(entityState)}
|
||||
>
|
||||
${!image && showIcon
|
||||
? html`<ha-state-icon
|
||||
@@ -188,13 +191,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
this.hass!.locale,
|
||||
getNumberFormatOptions(entityState, entry)
|
||||
)
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
entityState,
|
||||
this.hass!.locale,
|
||||
this.hass!.config,
|
||||
this.hass!.entities
|
||||
);
|
||||
: this.hass!.formatEntityState(entityState);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
|
||||
let jsYamlPromise: Promise<typeof import("../resources/js-yaml-dump")>;
|
||||
|
||||
@@ -14,11 +15,19 @@ class HaAttributeValue extends LitElement {
|
||||
|
||||
@property() public attribute!: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-unit" })
|
||||
public hideUnit?: boolean;
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
const attributeValue = this.stateObj.attributes[this.attribute];
|
||||
|
||||
if (typeof attributeValue === "number" && this.hideUnit) {
|
||||
return formatNumber(attributeValue, this.hass.locale);
|
||||
}
|
||||
|
||||
if (typeof attributeValue === "string") {
|
||||
// URL handling
|
||||
if (attributeValue.startsWith("http")) {
|
||||
|
@@ -1,12 +1,19 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
||||
import { STATE_ATTRIBUTES } from "../data/entity_attributes";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-attribute-value";
|
||||
import "./ha-expansion-panel";
|
||||
|
||||
@customElement("ha-attributes")
|
||||
class HaAttributes extends LitElement {
|
||||
@@ -18,16 +25,30 @@ class HaAttributes extends LitElement {
|
||||
|
||||
@state() private _expanded = false;
|
||||
|
||||
private get _filteredAttributes() {
|
||||
return this.computeDisplayAttributes(
|
||||
STATE_ATTRIBUTES.concat(
|
||||
this.extraFilters ? this.extraFilters.split(",") : []
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
changedProperties.has("extraFilters") ||
|
||||
changedProperties.has("stateObj")
|
||||
) {
|
||||
this.toggleAttribute("empty", this._filteredAttributes.length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const attributes = this.computeDisplayAttributes(
|
||||
STATE_ATTRIBUTES.concat(
|
||||
this.extraFilters ? this.extraFilters.split(",") : []
|
||||
)
|
||||
);
|
||||
const attributes = this._filteredAttributes;
|
||||
|
||||
if (attributes.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
|
@@ -1,11 +1,14 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { ClimateEntity, CLIMATE_PRESET_NONE } from "../data/climate";
|
||||
import { isUnavailableState } from "../data/entity";
|
||||
import { CLIMATE_PRESET_NONE, ClimateEntity } from "../data/climate";
|
||||
import { isUnavailableState, OFF } from "../data/entity";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-climate-state")
|
||||
@@ -24,26 +27,24 @@ class HaClimateState extends LitElement {
|
||||
${this.stateObj.attributes.preset_mode &&
|
||||
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||
? html`-
|
||||
${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
"preset_mode"
|
||||
)}`
|
||||
: ""}
|
||||
: nothing}
|
||||
</span>
|
||||
<div class="unit">${this._computeTarget()}</div>`
|
||||
: this._localizeState()}
|
||||
</div>
|
||||
|
||||
${currentStatus && !isUnavailableState(this.stateObj.state)
|
||||
? html`<div class="current">
|
||||
${this.hass.localize("ui.card.climate.currently")}:
|
||||
<div class="unit">${currentStatus}</div>
|
||||
</div>`
|
||||
: ""}`;
|
||||
? html`
|
||||
<div class="current">
|
||||
${this.hass.localize("ui.card.climate.currently")}:
|
||||
<div class="unit">${currentStatus}</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}`;
|
||||
}
|
||||
|
||||
private _computeCurrentStatus(): string | undefined {
|
||||
@@ -54,28 +55,28 @@ class HaClimateState extends LitElement {
|
||||
this.stateObj.attributes.current_temperature != null &&
|
||||
this.stateObj.attributes.current_humidity != null
|
||||
) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.current_temperature,
|
||||
this.hass.locale
|
||||
)} ${this.hass.config.unit_system.temperature}/
|
||||
${formatNumber(
|
||||
this.stateObj.attributes.current_humidity,
|
||||
this.hass.locale
|
||||
)}${blankBeforePercent(this.hass.locale)}%`;
|
||||
return `${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_temperature"
|
||||
)}/
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_humidity"
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_temperature != null) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.current_temperature,
|
||||
this.hass.locale
|
||||
)} ${this.hass.config.unit_system.temperature}`;
|
||||
return this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_temperature"
|
||||
);
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_humidity != null) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.current_humidity,
|
||||
this.hass.locale
|
||||
)}${blankBeforePercent(this.hass.locale)}%`;
|
||||
return this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_humidity"
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -90,39 +91,33 @@ class HaClimateState extends LitElement {
|
||||
this.stateObj.attributes.target_temp_low != null &&
|
||||
this.stateObj.attributes.target_temp_high != null
|
||||
) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.target_temp_low,
|
||||
this.hass.locale
|
||||
)}-${formatNumber(
|
||||
this.stateObj.attributes.target_temp_high,
|
||||
this.hass.locale
|
||||
)} ${this.hass.config.unit_system.temperature}`;
|
||||
return `${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"target_temp_low"
|
||||
)}-${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"target_temp_high"
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.temperature != null) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.temperature,
|
||||
this.hass.locale
|
||||
)} ${this.hass.config.unit_system.temperature}`;
|
||||
return this.hass.formatEntityAttributeValue(this.stateObj, "temperature");
|
||||
}
|
||||
if (
|
||||
this.stateObj.attributes.target_humidity_low != null &&
|
||||
this.stateObj.attributes.target_humidity_high != null
|
||||
) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.target_humidity_low,
|
||||
this.hass.locale
|
||||
)}-${formatNumber(
|
||||
this.stateObj.attributes.target_humidity_high,
|
||||
this.hass.locale
|
||||
)} %`;
|
||||
return `${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"target_humidity_low"
|
||||
)}-${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"target_humidity_high"
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.humidity != null) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.humidity,
|
||||
this.hass.locale
|
||||
)} %`;
|
||||
return this.hass.formatEntityAttributeValue(this.stateObj, "humidity");
|
||||
}
|
||||
|
||||
return "";
|
||||
@@ -133,24 +128,17 @@ class HaClimateState extends LitElement {
|
||||
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||
}
|
||||
|
||||
const stateString = computeStateDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities
|
||||
);
|
||||
const stateString = this.hass.formatEntityState(this.stateObj);
|
||||
|
||||
return this.stateObj.attributes.hvac_action
|
||||
? `${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
"hvac_action"
|
||||
)} (${stateString})`
|
||||
: stateString;
|
||||
if (this.stateObj.attributes.hvac_action && this.stateObj.state !== OFF) {
|
||||
const actionString = this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"hvac_action"
|
||||
);
|
||||
return `${actionString} (${stateString})`;
|
||||
}
|
||||
|
||||
return stateString;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -244,7 +244,6 @@ export class HaComboBox extends LitElement {
|
||||
);
|
||||
|
||||
if (overlay) {
|
||||
overlay.setAttribute("required-vertical-space", "0");
|
||||
this._removeInert(overlay);
|
||||
}
|
||||
this._observeBody();
|
||||
@@ -312,6 +311,10 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!this.allowCustomValue) {
|
||||
// @ts-ignore
|
||||
this._comboBox._closeOnBlurIsPrevented = true;
|
||||
}
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (newValue !== this.value) {
|
||||
@@ -327,7 +330,7 @@ export class HaComboBox extends LitElement {
|
||||
}
|
||||
vaadin-combo-box-light {
|
||||
position: relative;
|
||||
--vaadin-combo-box-overlay-max-height: calc(45vh);
|
||||
--vaadin-combo-box-overlay-max-height: calc(45vh - 56px);
|
||||
}
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
|
@@ -59,6 +59,8 @@ const A11Y_KEY_CODES = new Set([
|
||||
"End",
|
||||
]);
|
||||
|
||||
export type ControlCircularSliderMode = "start" | "end" | "full";
|
||||
|
||||
@customElement("ha-control-circular-slider")
|
||||
export class HaControlCircularSlider extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
@@ -67,8 +69,11 @@ export class HaControlCircularSlider extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
public dual?: boolean;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public inverted?: boolean;
|
||||
@property({ type: String })
|
||||
public mode?: ControlCircularSliderMode;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public inactive?: boolean;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
@@ -407,12 +412,10 @@ export class HaControlCircularSlider extends LitElement {
|
||||
protected renderArc(
|
||||
id: string,
|
||||
value: number | undefined,
|
||||
inverted: boolean | undefined
|
||||
mode: ControlCircularSliderMode
|
||||
) {
|
||||
if (this.disabled) return nothing;
|
||||
|
||||
const limit = inverted ? this.max : this.min;
|
||||
|
||||
const path = svgArc({
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -421,82 +424,100 @@ export class HaControlCircularSlider extends LitElement {
|
||||
r: RADIUS,
|
||||
});
|
||||
|
||||
const limit = mode === "end" ? this.max : this.min;
|
||||
|
||||
const current = this.current ?? limit;
|
||||
const target = value ?? limit;
|
||||
|
||||
const showActive = inverted ? target <= current : current <= target;
|
||||
const showActive =
|
||||
mode === "end"
|
||||
? target <= current
|
||||
: mode === "start"
|
||||
? current <= target
|
||||
: false;
|
||||
|
||||
const activeArcDashArray = showActive
|
||||
? inverted
|
||||
const activeArc = showActive
|
||||
? mode === "end"
|
||||
? this._strokeDashArc(target, current)
|
||||
: this._strokeDashArc(current, target)
|
||||
: this._strokeCircleDashArc(target);
|
||||
|
||||
const arcDashArray = inverted
|
||||
? this._strokeDashArc(target, limit)
|
||||
: this._strokeDashArc(limit, target);
|
||||
const coloredArc =
|
||||
mode === "full"
|
||||
? this._strokeDashArc(this.min, this.max)
|
||||
: mode === "end"
|
||||
? this._strokeDashArc(target, limit)
|
||||
: this._strokeDashArc(limit, target);
|
||||
|
||||
const targetCircleDashArray = this._strokeCircleDashArc(target);
|
||||
const targetCircle = this._strokeCircleDashArc(target);
|
||||
|
||||
const currentCircleDashArray =
|
||||
const currentCircle =
|
||||
this.current != null &&
|
||||
showActive &&
|
||||
current <= this.max &&
|
||||
current >= this.min
|
||||
this.current <= this.max &&
|
||||
this.current >= this.min &&
|
||||
(showActive || this.mode === "full")
|
||||
? this._strokeCircleDashArc(this.current)
|
||||
: undefined;
|
||||
|
||||
return svg`
|
||||
<path
|
||||
class="arc arc-clear"
|
||||
d=${path}
|
||||
stroke-dasharray=${arcDashArray[0]}
|
||||
stroke-dashoffset=${arcDashArray[1]}
|
||||
/>
|
||||
<path
|
||||
class="arc arc-background ${classMap({ [id]: true })}"
|
||||
d=${path}
|
||||
stroke-dasharray=${arcDashArray[0]}
|
||||
stroke-dashoffset=${arcDashArray[1]}
|
||||
/>
|
||||
<path
|
||||
.id=${id}
|
||||
d=${path}
|
||||
class="arc arc-active ${classMap({ [id]: true })}"
|
||||
stroke-dasharray=${activeArcDashArray[0]}
|
||||
stroke-dashoffset=${activeArcDashArray[1]}
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuemin=${this.min}
|
||||
aria-valuemax=${this.max}
|
||||
aria-valuenow=${
|
||||
this._localValue != null
|
||||
? this._steppedValue(this._localValue)
|
||||
: undefined
|
||||
<g class=${classMap({ inactive: Boolean(this.inactive) })}>
|
||||
<path
|
||||
class="arc arc-clear"
|
||||
d=${path}
|
||||
stroke-dasharray=${coloredArc[0]}
|
||||
stroke-dashoffset=${coloredArc[1]}
|
||||
/>
|
||||
<path
|
||||
class="arc arc-colored ${classMap({ [id]: true })}"
|
||||
d=${path}
|
||||
stroke-dasharray=${coloredArc[0]}
|
||||
stroke-dashoffset=${coloredArc[1]}
|
||||
/>
|
||||
<path
|
||||
.id=${id}
|
||||
d=${path}
|
||||
class="arc arc-active ${classMap({ [id]: true })}"
|
||||
stroke-dasharray=${activeArc[0]}
|
||||
stroke-dashoffset=${activeArc[1]}
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuemin=${this.min}
|
||||
aria-valuemax=${this.max}
|
||||
aria-valuenow=${
|
||||
this._localValue != null
|
||||
? this._steppedValue(this._localValue)
|
||||
: undefined
|
||||
}
|
||||
aria-disabled=${this.disabled}
|
||||
aria-label=${ifDefined(this.lowLabel ?? this.label)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@keyup=${this._handleKeyUp}
|
||||
/>
|
||||
${
|
||||
currentCircle
|
||||
? svg`
|
||||
<path
|
||||
class="current arc-current"
|
||||
d=${path}
|
||||
stroke-dasharray=${currentCircle[0]}
|
||||
stroke-dashoffset=${currentCircle[1]}
|
||||
/>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
aria-disabled=${this.disabled}
|
||||
aria-label=${ifDefined(this.lowLabel ?? this.label)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@keyup=${this._handleKeyUp}
|
||||
/>
|
||||
${
|
||||
currentCircleDashArray
|
||||
? svg`
|
||||
<path
|
||||
class="current arc-current"
|
||||
d=${path}
|
||||
stroke-dasharray=${currentCircleDashArray[0]}
|
||||
stroke-dashoffset=${currentCircleDashArray[1]}
|
||||
/>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<path
|
||||
class="target"
|
||||
d=${path}
|
||||
stroke-dasharray=${targetCircleDashArray[0]}
|
||||
stroke-dashoffset=${targetCircleDashArray[1]}
|
||||
/>
|
||||
<path
|
||||
class="target-border ${classMap({ [id]: true })}"
|
||||
d=${path}
|
||||
stroke-dasharray=${targetCircle[0]}
|
||||
stroke-dashoffset=${targetCircle[1]}
|
||||
/>
|
||||
<path
|
||||
class="target"
|
||||
d=${path}
|
||||
stroke-dasharray=${targetCircle[0]}
|
||||
stroke-dashoffset=${targetCircle[1]}
|
||||
/>
|
||||
</g>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -551,11 +572,11 @@ export class HaControlCircularSlider extends LitElement {
|
||||
? this.renderArc(
|
||||
this.dual ? "low" : "value",
|
||||
lowValue,
|
||||
this.inverted
|
||||
(!this.dual && this.mode) || "start"
|
||||
)
|
||||
: nothing}
|
||||
${this.dual && highValue != null
|
||||
? this.renderArc("high", highValue, true)
|
||||
? this.renderArc("high", highValue, "end")
|
||||
: nothing}
|
||||
</g>
|
||||
</g>
|
||||
@@ -634,6 +655,19 @@ export class HaControlCircularSlider extends LitElement {
|
||||
opacity 180ms ease-in-out;
|
||||
}
|
||||
|
||||
.target-border {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 24px;
|
||||
stroke: white;
|
||||
transition:
|
||||
stroke-width 300ms ease-in-out,
|
||||
stroke-dasharray 300ms ease-in-out,
|
||||
stroke-dashoffset 300ms ease-in-out,
|
||||
stroke 180ms ease-in-out,
|
||||
opacity 180ms ease-in-out;
|
||||
}
|
||||
|
||||
.current {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
@@ -655,7 +689,7 @@ export class HaControlCircularSlider extends LitElement {
|
||||
.arc-clear {
|
||||
stroke: var(--clear-background-color);
|
||||
}
|
||||
.arc-background {
|
||||
.arc-colored {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.arc-active {
|
||||
@@ -667,6 +701,7 @@ export class HaControlCircularSlider extends LitElement {
|
||||
|
||||
.pressed .arc,
|
||||
.pressed .target,
|
||||
.pressed .target-border,
|
||||
.pressed .current {
|
||||
transition:
|
||||
stroke-width 300ms ease-in-out,
|
||||
@@ -674,6 +709,11 @@ export class HaControlCircularSlider extends LitElement {
|
||||
opacity 180ms ease-in-out;
|
||||
}
|
||||
|
||||
.inactive .arc,
|
||||
.inactive .arc-current {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
stroke: var(--control-circular-slider-color);
|
||||
}
|
||||
|
260
src/components/ha-control-number-buttons.ts
Normal file
260
src/components/ha-control-number-buttons.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
import { mdiMinus, mdiPlus } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { conditionalClamp } from "../common/number/clamp";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { FrontendLocaleData } from "../data/translation";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
const A11Y_KEY_CODES = new Set([
|
||||
"ArrowRight",
|
||||
"ArrowUp",
|
||||
"ArrowLeft",
|
||||
"ArrowDown",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"Home",
|
||||
"End",
|
||||
]);
|
||||
|
||||
@customElement("ha-control-number-buttons")
|
||||
export class HaControlNumberButton extends LitElement {
|
||||
@property({ attribute: false }) public locale?: FrontendLocaleData;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Number }) public step?: number;
|
||||
|
||||
@property({ type: Number }) public value?: number;
|
||||
|
||||
@property({ type: Number }) public min?: number;
|
||||
|
||||
@property({ type: Number }) public max?: number;
|
||||
|
||||
@property({ attribute: "false" })
|
||||
public formatOptions: Intl.NumberFormatOptions = {};
|
||||
|
||||
@query("#input") _input!: HTMLDivElement;
|
||||
|
||||
private boundedValue(value: number) {
|
||||
const clamped = conditionalClamp(value, this.min, this.max);
|
||||
return Math.round(clamped / this._step) * this._step;
|
||||
}
|
||||
|
||||
private get _step() {
|
||||
return this.step ?? 1;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value ?? 0;
|
||||
}
|
||||
|
||||
private get _tenPercentStep() {
|
||||
if (this.max == null || this.min == null) return this._step;
|
||||
const range = this.max - this.min / 10;
|
||||
|
||||
if (range <= this._step) return this._step;
|
||||
return Math.max(range / 10);
|
||||
}
|
||||
|
||||
private _handlePlusButton() {
|
||||
this._increment();
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
this._input.focus();
|
||||
}
|
||||
|
||||
private _handleMinusButton() {
|
||||
this._decrement();
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
this._input.focus();
|
||||
}
|
||||
|
||||
private _increment() {
|
||||
this.value = this.boundedValue(this._value + this._step);
|
||||
}
|
||||
|
||||
private _decrement() {
|
||||
this.value = this.boundedValue(this._value - this._step);
|
||||
}
|
||||
|
||||
_handleKeyDown(e: KeyboardEvent) {
|
||||
if (this.disabled) return;
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
e.preventDefault();
|
||||
switch (e.code) {
|
||||
case "ArrowRight":
|
||||
case "ArrowUp":
|
||||
this._increment();
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
case "ArrowDown":
|
||||
this._decrement();
|
||||
break;
|
||||
case "PageUp":
|
||||
this.value = this.boundedValue(this._value + this._tenPercentStep);
|
||||
break;
|
||||
case "PageDown":
|
||||
this.value = this.boundedValue(this._value - this._tenPercentStep);
|
||||
break;
|
||||
case "Home":
|
||||
if (this.min != null) {
|
||||
this.value = this.min;
|
||||
}
|
||||
break;
|
||||
case "End":
|
||||
if (this.max != null) {
|
||||
this.value = this.max;
|
||||
}
|
||||
break;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const displayedValue =
|
||||
this.value != null
|
||||
? formatNumber(this.value, this.locale, this.formatOptions)
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<div
|
||||
id="input"
|
||||
class="value"
|
||||
role="number-button"
|
||||
.tabIndex=${this.disabled ? "-1" : "0"}
|
||||
aria-valuenow=${this.value}
|
||||
aria-valuemin=${this.min}
|
||||
aria-valuemax=${this.max}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
?disabled=${this.disabled}
|
||||
@keydown=${this._handleKeyDown}
|
||||
>
|
||||
${displayedValue}
|
||||
</div>
|
||||
<button
|
||||
class="button minus"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
aria-label="decrement"
|
||||
@click=${this._handleMinusButton}
|
||||
.disabled=${this.disabled ||
|
||||
(this.min != null && this._value <= this.min)}
|
||||
>
|
||||
<ha-svg-icon aria-hidden .path=${mdiMinus}></ha-svg-icon>
|
||||
</button>
|
||||
<button
|
||||
class="button plus"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
aria-label="increment"
|
||||
@click=${this._handlePlusButton}
|
||||
.disabled=${this.disabled ||
|
||||
(this.max != null && this._value >= this.max)}
|
||||
>
|
||||
<ha-svg-icon aria-hidden .path=${mdiPlus}></ha-svg-icon>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
--control-number-buttons-focus-color: var(--primary-color);
|
||||
--control-number-buttons-background-color: var(--disabled-color);
|
||||
--control-number-buttons-background-opacity: 0.2;
|
||||
--control-number-buttons-border-radius: 10px;
|
||||
--mdc-icon-size: 16px;
|
||||
height: 40px;
|
||||
width: 200px;
|
||||
color: var(--primary-text-color);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
transition: color 180ms ease-in-out;
|
||||
}
|
||||
:host([disabled]) {
|
||||
color: var(--disabled-color);
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 44px;
|
||||
border-radius: var(--control-number-buttons-border-radius);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
/* For safari border-radius overflow */
|
||||
z-index: 0;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
outline: none;
|
||||
}
|
||||
.value::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--control-number-buttons-background-color);
|
||||
transition:
|
||||
background-color 180ms ease-in-out,
|
||||
opacity 180ms ease-in-out;
|
||||
opacity: var(--control-number-buttons-background-opacity);
|
||||
}
|
||||
.value:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--control-number-buttons-focus-color);
|
||||
}
|
||||
.button {
|
||||
color: inherit;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
padding: 0;
|
||||
width: 35px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.button[disabled] {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.button.minus {
|
||||
left: 0;
|
||||
}
|
||||
.button.plus {
|
||||
right: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-control-number-buttons": HaControlNumberButton;
|
||||
}
|
||||
}
|
@@ -13,6 +13,10 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import "./ha-icon";
|
||||
import type { HaIcon } from "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
import type { HaSvgIcon } from "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-control-select-menu")
|
||||
export class HaControlSelectMenu extends SelectBase {
|
||||
@@ -66,9 +70,7 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
@touchend=${this.handleRippleDeactivate}
|
||||
@touchcancel=${this.handleRippleDeactivate}
|
||||
>
|
||||
<div class="icon">
|
||||
<slot name="icon"></slot>
|
||||
</div>
|
||||
${this.renderIcon()}
|
||||
<div class="content">
|
||||
<p id="label" class="label">${this.label}</p>
|
||||
${this.selectedText
|
||||
@@ -84,6 +86,25 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderIcon() {
|
||||
const index = this.mdcFoundation?.getSelectedIndex();
|
||||
const items = this.menuElement?.items ?? [];
|
||||
const item = index != null ? items[index] : undefined;
|
||||
const icon =
|
||||
item?.querySelector("[slot='graphic']") ??
|
||||
(null as HaSvgIcon | HaIcon | null);
|
||||
|
||||
return html`
|
||||
<div class="icon">
|
||||
${icon && "path" in icon
|
||||
? html`<ha-svg-icon .path=${icon.path}></ha-svg-icon>`
|
||||
: icon && "icon" in icon
|
||||
? html`<ha-icon .path=${icon.icon}></ha-icon>`
|
||||
: html`<slot name="icon"></slot>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected onFocus() {
|
||||
this.handleRippleFocus();
|
||||
super.onFocus();
|
||||
@@ -149,18 +170,15 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
--control-select-menu-text-color: var(--primary-text-color);
|
||||
--control-select-menu-background-color: var(--disabled-color);
|
||||
--control-select-menu-background-opacity: 0.2;
|
||||
--control-select-menu-border-radius: 16px;
|
||||
--control-select-menu-min-width: 120px;
|
||||
--control-select-menu-max-width: 200px;
|
||||
--control-select-menu-width: 100%;
|
||||
--mdc-icon-size: 24px;
|
||||
--control-select-menu-border-radius: 14px;
|
||||
--mdc-icon-size: 20px;
|
||||
width: auto;
|
||||
color: var(--primary-text-color);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.select-anchor {
|
||||
color: var(--control-select-menu-text-color);
|
||||
height: 56px;
|
||||
padding: 8px 12px;
|
||||
height: 48px;
|
||||
padding: 6px 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
@@ -177,12 +195,14 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
z-index: 0;
|
||||
font-size: inherit;
|
||||
transition: color 180ms ease-in-out;
|
||||
color: var(--control-text-icon-color);
|
||||
gap: 12px;
|
||||
min-width: var(--control-select-menu-min-width);
|
||||
max-width: var(--control-select-menu-max-width);
|
||||
width: var(--control-select-menu-width);
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.25px;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
@@ -204,24 +224,14 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.select-no-value .label {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0.5px;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
letter-spacing: inherit;
|
||||
}
|
||||
|
||||
.select-anchor::before {
|
||||
|
@@ -217,6 +217,7 @@ export class HaControlSelect extends LitElement {
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
color: var(--primary-text-color);
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
@@ -267,7 +268,6 @@ export class HaControlSelect extends LitElement {
|
||||
justify-content: center;
|
||||
border-radius: var(--control-select-button-border-radius);
|
||||
overflow: hidden;
|
||||
color: var(--primary-text-color);
|
||||
/* For safari border-radius overflow */
|
||||
z-index: 0;
|
||||
}
|
||||
@@ -331,6 +331,7 @@ export class HaControlSelect extends LitElement {
|
||||
:host([disabled]) {
|
||||
--control-select-color: var(--disabled-color);
|
||||
--control-select-focused-opacity: 0;
|
||||
color: var(--disabled-color);
|
||||
}
|
||||
:host([disabled]) .option {
|
||||
cursor: not-allowed;
|
||||
|
@@ -43,6 +43,9 @@ export class HaControlSlider extends LitElement {
|
||||
@property({ type: Boolean, attribute: "show-handle" })
|
||||
public showHandle = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "inverted" })
|
||||
public inverted = false;
|
||||
|
||||
@property({ type: Number })
|
||||
public value?: number;
|
||||
|
||||
@@ -61,11 +64,16 @@ export class HaControlSlider extends LitElement {
|
||||
public pressed = false;
|
||||
|
||||
valueToPercentage(value: number) {
|
||||
return (this.boundedValue(value) - this.min) / (this.max - this.min);
|
||||
const percentage =
|
||||
(this.boundedValue(value) - this.min) / (this.max - this.min);
|
||||
return this.inverted ? 1 - percentage : percentage;
|
||||
}
|
||||
|
||||
percentageToValue(value: number) {
|
||||
return (this.max - this.min) * value + this.min;
|
||||
percentageToValue(percentage: number) {
|
||||
return (
|
||||
(this.max - this.min) * (this.inverted ? 1 - percentage : percentage) +
|
||||
this.min
|
||||
);
|
||||
}
|
||||
|
||||
steppedValue(value: number) {
|
||||
|
@@ -155,11 +155,12 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
if (!this._configEntry) {
|
||||
return;
|
||||
}
|
||||
showOptionsFlowDialog(
|
||||
this,
|
||||
this._configEntry,
|
||||
await fetchIntegrationManifest(this.hass, this._configEntry.domain)
|
||||
);
|
||||
showOptionsFlowDialog(this, this._configEntry, {
|
||||
manifest: await fetchIntegrationManifest(
|
||||
this.hass,
|
||||
this._configEntry.domain
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -10,12 +10,12 @@ import "./ha-icon-button";
|
||||
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
|
||||
|
||||
export const createCloseHeading = (
|
||||
hass: HomeAssistant,
|
||||
hass: HomeAssistant | undefined,
|
||||
title: string | TemplateResult
|
||||
) => html`
|
||||
<div class="header_title">${title}</div>
|
||||
<ha-icon-button
|
||||
.label=${hass.localize("ui.dialogs.generic.close")}
|
||||
.label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
class="header_button"
|
||||
|
@@ -1,16 +1,19 @@
|
||||
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { mdiDelete, mdiFileUpload } from "@mdi/js";
|
||||
import { LitElement, PropertyValues, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-circular-progress";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"file-picked": { files: FileList };
|
||||
"file-picked": { files: File[] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +25,22 @@ export class HaFileUpload extends LitElement {
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@property() public label!: string;
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value: string | TemplateResult | null = null;
|
||||
@property() public secondary?: string;
|
||||
|
||||
@property() public supports?: string;
|
||||
|
||||
@property() public value?: File | File[] | FileList | string;
|
||||
|
||||
@property({ type: Boolean }) private multiple = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled: boolean = false;
|
||||
|
||||
@property({ type: Boolean }) private uploading = false;
|
||||
|
||||
@property({ type: Number }) private progress?: number;
|
||||
|
||||
@property({ type: Boolean, attribute: "auto-open-file-dialog" })
|
||||
private autoOpenFileDialog = false;
|
||||
|
||||
@@ -45,72 +58,102 @@ export class HaFileUpload extends LitElement {
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${this.uploading
|
||||
? html`<ha-circular-progress
|
||||
alt="Uploading"
|
||||
size="large"
|
||||
active
|
||||
></ha-circular-progress>`
|
||||
: html`
|
||||
<label
|
||||
for="input"
|
||||
class="mdc-text-field mdc-text-field--filled ${classMap({
|
||||
"mdc-text-field--focused": this._drag,
|
||||
"mdc-text-field--with-leading-icon": Boolean(this.icon),
|
||||
"mdc-text-field--with-trailing-icon": Boolean(this.value),
|
||||
})}"
|
||||
@drop=${this._handleDrop}
|
||||
@dragenter=${this._handleDragStart}
|
||||
@dragover=${this._handleDragStart}
|
||||
@dragleave=${this._handleDragEnd}
|
||||
@dragend=${this._handleDragEnd}
|
||||
>
|
||||
<span class="mdc-text-field__ripple"></span>
|
||||
<span
|
||||
class="mdc-floating-label ${this.value || this._drag
|
||||
? "mdc-floating-label--float-above"
|
||||
: ""}"
|
||||
id="label"
|
||||
>${this.label}</span
|
||||
? html`<div class="container">
|
||||
<div class="row">
|
||||
<span class="header"
|
||||
>${this.value
|
||||
? this.hass?.localize(
|
||||
"ui.components.file-upload.uploading_name",
|
||||
{ name: this.value }
|
||||
)
|
||||
: this.hass?.localize(
|
||||
"ui.components.file-upload.uploading"
|
||||
)}</span
|
||||
>
|
||||
${this.icon
|
||||
? html`<span
|
||||
class="mdc-text-field__icon mdc-text-field__icon--leading"
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._openFilePicker}
|
||||
.path=${this.icon}
|
||||
></ha-icon-button>
|
||||
</span>`
|
||||
${this.progress
|
||||
? html`<span class="progress"
|
||||
>${this.progress}${blankBeforePercent(
|
||||
this.hass!.locale
|
||||
)}%</span
|
||||
>`
|
||||
: ""}
|
||||
<div class="value">${this.value}</div>
|
||||
<input
|
||||
id="input"
|
||||
type="file"
|
||||
class="mdc-text-field__input file"
|
||||
accept=${this.accept}
|
||||
@change=${this._handleFilePicked}
|
||||
aria-labelledby="label"
|
||||
/>
|
||||
${this.value
|
||||
? html`<span
|
||||
class="mdc-text-field__icon mdc-text-field__icon--trailing"
|
||||
</div>
|
||||
<mwc-linear-progress
|
||||
.indeterminate=${!this.progress}
|
||||
.progress=${this.progress ? this.progress / 100 : undefined}
|
||||
></mwc-linear-progress>
|
||||
</div>`
|
||||
: html`<label
|
||||
for=${this.value ? "" : "input"}
|
||||
class="container ${classMap({
|
||||
dragged: this._drag,
|
||||
multiple: this.multiple,
|
||||
value: Boolean(this.value),
|
||||
})}"
|
||||
@drop=${this._handleDrop}
|
||||
@dragenter=${this._handleDragStart}
|
||||
@dragover=${this._handleDragStart}
|
||||
@dragleave=${this._handleDragEnd}
|
||||
@dragend=${this._handleDragEnd}
|
||||
>${!this.value
|
||||
? html`<ha-svg-icon
|
||||
class="big-icon"
|
||||
.path=${this.icon || mdiFileUpload}
|
||||
></ha-svg-icon>
|
||||
<ha-button unelevated @click=${this._openFilePicker}>
|
||||
${this.label ||
|
||||
this.hass?.localize("ui.components.file-upload.label")}
|
||||
</ha-button>
|
||||
<span class="secondary"
|
||||
>${this.secondary ||
|
||||
this.hass?.localize(
|
||||
"ui.components.file-upload.secondary"
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="suffix"
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.close") ||
|
||||
"close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</span>`
|
||||
: ""}
|
||||
<span
|
||||
class="mdc-line-ripple ${this._drag
|
||||
? "mdc-line-ripple--active"
|
||||
: ""}"
|
||||
></span>
|
||||
</label>
|
||||
`}
|
||||
<span class="supports">${this.supports}</span>`
|
||||
: typeof this.value === "string"
|
||||
? html`<div class="row">
|
||||
<div class="value" @click=${this._openFilePicker}>
|
||||
<ha-svg-icon
|
||||
.path=${this.icon || mdiFileUpload}
|
||||
></ha-svg-icon>
|
||||
${this.value}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
: (this.value instanceof FileList
|
||||
? Array.from(this.value)
|
||||
: ensureArray(this.value)
|
||||
).map(
|
||||
(file) =>
|
||||
html`<div class="row">
|
||||
<div class="value" @click=${this._openFilePicker}>
|
||||
<ha-svg-icon
|
||||
.path=${this.icon || mdiFileUpload}
|
||||
></ha-svg-icon>
|
||||
${file.name} - ${bytesToString(file.size)}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
)}
|
||||
<input
|
||||
id="input"
|
||||
type="file"
|
||||
class="file"
|
||||
.accept=${this.accept}
|
||||
.multiple=${this.multiple}
|
||||
@change=${this._handleFilePicked}
|
||||
/></label>`}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -122,7 +165,12 @@ export class HaFileUpload extends LitElement {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (ev.dataTransfer?.files) {
|
||||
fireEvent(this, "file-picked", { files: ev.dataTransfer.files });
|
||||
fireEvent(this, "file-picked", {
|
||||
files:
|
||||
this.multiple || ev.dataTransfer.files.length === 1
|
||||
? Array.from(ev.dataTransfer.files)
|
||||
: [ev.dataTransfer.files[0]],
|
||||
});
|
||||
}
|
||||
this._drag = false;
|
||||
}
|
||||
@@ -140,92 +188,121 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
|
||||
private _handleFilePicked(ev) {
|
||||
if (ev.target.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.value = ev.target.files;
|
||||
fireEvent(this, "file-picked", { files: ev.target.files });
|
||||
}
|
||||
|
||||
private _clearValue(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this.value = null;
|
||||
this._input!.value = "";
|
||||
this.value = undefined;
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.mdc-text-field--filled {
|
||||
height: auto;
|
||||
padding-top: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mdc-text-field--filled.mdc-text-field--with-trailing-icon {
|
||||
padding-top: 28px;
|
||||
}
|
||||
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.mdc-text-field--filled.mdc-text-field--with-trailing-icon
|
||||
.mdc-text-field__icon {
|
||||
align-self: flex-end;
|
||||
}
|
||||
.mdc-text-field__icon--leading {
|
||||
margin-bottom: 12px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 0px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-text-field--filled .mdc-floating-label--float-above {
|
||||
transform: scale(0.75);
|
||||
top: 8px;
|
||||
}
|
||||
.mdc-floating-label {
|
||||
inset-inline-start: 16px !important;
|
||||
inset-inline-end: initial !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-text-field--filled .mdc-floating-label {
|
||||
inset-inline-start: 48px !important;
|
||||
inset-inline-end: initial !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-text-field__icon--trailing {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
.dragged:before {
|
||||
position: var(--layout-fit_-_position);
|
||||
top: var(--layout-fit_-_top);
|
||||
right: var(--layout-fit_-_right);
|
||||
bottom: var(--layout-fit_-_bottom);
|
||||
left: var(--layout-fit_-_left);
|
||||
background: currentColor;
|
||||
content: "";
|
||||
opacity: var(--dark-divider-opacity);
|
||||
pointer-events: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.value {
|
||||
width: 100%;
|
||||
}
|
||||
input.file {
|
||||
display: none;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 125px;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
text-align-last: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 240px;
|
||||
}
|
||||
:host([disabled]) {
|
||||
pointer-events: none;
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: solid 1px
|
||||
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
||||
border-radius: var(--mdc-shape-small, 4px);
|
||||
height: 100%;
|
||||
}
|
||||
label.container {
|
||||
border: dashed 1px
|
||||
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
||||
cursor: pointer;
|
||||
}
|
||||
:host([disabled]) .container {
|
||||
border-color: var(--disabled-color);
|
||||
}
|
||||
label.dragged {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.dragged:before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: var(--primary-color);
|
||||
content: "";
|
||||
opacity: var(--dark-divider-opacity);
|
||||
pointer-events: none;
|
||||
border-radius: var(--mdc-shape-small, 4px);
|
||||
}
|
||||
label.value {
|
||||
cursor: default;
|
||||
}
|
||||
label.value.multiple {
|
||||
justify-content: unset;
|
||||
overflow: auto;
|
||||
}
|
||||
.highlight {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
ha-button {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.supports {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
:host([disabled]) .secondary {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
input.file {
|
||||
display: none;
|
||||
}
|
||||
.value {
|
||||
cursor: pointer;
|
||||
}
|
||||
.value ha-svg-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.big-icon {
|
||||
--mdc-icon-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-button {
|
||||
--mdc-button-outline-color: var(--primary-color);
|
||||
--mdc-icon-button-size: 24px;
|
||||
}
|
||||
mwc-linear-progress {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header {
|
||||
font-weight: 500;
|
||||
}
|
||||
.progress {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -27,7 +27,8 @@ export const computeInitialHaFormData = (
|
||||
data[field.name] = 0.0;
|
||||
} else if (field.type === "select") {
|
||||
if (field.options.length) {
|
||||
data[field.name] = field.options[0][0];
|
||||
const val = field.options[0];
|
||||
data[field.name] = Array.isArray(val) ? val[0] : val;
|
||||
}
|
||||
} else if (field.type === "positive_time_period_dict") {
|
||||
data[field.name] = {
|
||||
@@ -47,11 +48,12 @@ export const computeInitialHaFormData = (
|
||||
} else if ("boolean" in selector) {
|
||||
data[field.name] = false;
|
||||
} else if (
|
||||
"text" in selector ||
|
||||
"addon" in selector ||
|
||||
"attribute" in selector ||
|
||||
"file" in selector ||
|
||||
"icon" in selector ||
|
||||
"template" in selector ||
|
||||
"text" in selector ||
|
||||
"theme" in selector
|
||||
) {
|
||||
data[field.name] = "";
|
||||
@@ -59,7 +61,8 @@ export const computeInitialHaFormData = (
|
||||
data[field.name] = selector.number?.min ?? 0;
|
||||
} else if ("select" in selector) {
|
||||
if (selector.select?.options.length) {
|
||||
data[field.name] = selector.select.options[0][0];
|
||||
const val = selector.select.options[0];
|
||||
data[field.name] = typeof val === "string" ? val : val.value;
|
||||
}
|
||||
} else if ("duration" in selector) {
|
||||
data[field.name] = {
|
||||
|
@@ -68,6 +68,7 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
: this.schema.description?.suffix}
|
||||
.validationMessage=${this.schema.required ? "Required" : undefined}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
${isPassword
|
||||
? html`<ha-icon-button
|
||||
|
@@ -7,6 +7,12 @@ import { hsv2rgb, rgb2hex } from "../common/color/convert-color";
|
||||
import { rgbw2rgb, rgbww2rgb } from "../common/color/convert-light-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"cursor-moved": { value?: any };
|
||||
}
|
||||
}
|
||||
|
||||
function xy2polar(x: number, y: number) {
|
||||
const r = Math.sqrt(x * x + y * y);
|
||||
const phi = Math.atan2(y, x);
|
||||
|
@@ -1,9 +1,5 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { isUnavailableState, OFF } from "../data/entity";
|
||||
import { HumidifierEntity } from "../data/humidifier";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -23,12 +19,8 @@ class HaHumidifierState extends LitElement {
|
||||
${this._localizeState()}
|
||||
${this.stateObj.attributes.mode
|
||||
? html`-
|
||||
${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
"mode"
|
||||
)}`
|
||||
: ""}
|
||||
@@ -51,10 +43,10 @@ class HaHumidifierState extends LitElement {
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_humidity != null) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.current_humidity,
|
||||
this.hass.locale
|
||||
)}${blankBeforePercent(this.hass.locale)}%`;
|
||||
return `${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_humidity"
|
||||
)}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -66,10 +58,10 @@ class HaHumidifierState extends LitElement {
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.humidity != null) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.humidity,
|
||||
this.hass.locale
|
||||
)}${blankBeforePercent(this.hass.locale)}%`;
|
||||
return `${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"humidity"
|
||||
)}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
@@ -80,24 +72,17 @@ class HaHumidifierState extends LitElement {
|
||||
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||
}
|
||||
|
||||
const stateString = computeStateDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities
|
||||
);
|
||||
const stateString = this.hass.formatEntityState(this.stateObj);
|
||||
|
||||
return this.stateObj.attributes.action && this.stateObj.state !== OFF
|
||||
? `${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
"action"
|
||||
)} (${stateString})`
|
||||
: stateString;
|
||||
if (this.stateObj.attributes.action && this.stateObj.state !== OFF) {
|
||||
const actionString = this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"action"
|
||||
);
|
||||
return `${actionString} (${stateString})`;
|
||||
}
|
||||
|
||||
return stateString;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -14,17 +14,17 @@ export class HaIconButtonGroup extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 56px;
|
||||
height: 48px;
|
||||
border-radius: 28px;
|
||||
background-color: rgba(139, 145, 151, 0.1);
|
||||
box-sizing: border-box;
|
||||
width: auto;
|
||||
padding: 4px;
|
||||
gap: 4px;
|
||||
padding: 0;
|
||||
}
|
||||
::slotted(.separator) {
|
||||
background-color: rgba(var(--rgb-primary-text-color), 0.15);
|
||||
width: 1px;
|
||||
margin: 0 1px;
|
||||
height: 40px;
|
||||
}
|
||||
`;
|
||||
|
@@ -7,6 +7,7 @@ import { formatLanguageCode } from "../common/language/format_language";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { FrontendLocaleData } from "../data/translation";
|
||||
import "../resources/intl-polyfill";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
@@ -20,7 +21,7 @@ export class HaLanguagePicker extends LitElement {
|
||||
|
||||
@property() public languages?: string[];
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@@ -41,7 +42,18 @@ export class HaLanguagePicker extends LitElement {
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("languages") || changedProperties.has("value")) {
|
||||
|
||||
const localeChanged =
|
||||
changedProperties.has("hass") &&
|
||||
this.hass &&
|
||||
changedProperties.get("hass") &&
|
||||
changedProperties.get("hass").locale.language !==
|
||||
this.hass.locale.language;
|
||||
if (
|
||||
changedProperties.has("languages") ||
|
||||
changedProperties.has("value") ||
|
||||
localeChanged
|
||||
) {
|
||||
this._select.layoutOptions();
|
||||
if (this._select.value !== this.value) {
|
||||
fireEvent(this, "value-changed", { value: this._select.value });
|
||||
@@ -51,24 +63,27 @@ export class HaLanguagePicker extends LitElement {
|
||||
}
|
||||
const languageOptions = this._getLanguagesOptions(
|
||||
this.languages ?? this._defaultLanguages,
|
||||
this.hass.locale,
|
||||
this.nativeName
|
||||
this.nativeName,
|
||||
this.hass?.locale
|
||||
);
|
||||
const selectedItem = languageOptions.find(
|
||||
const selectedItemIndex = languageOptions.findIndex(
|
||||
(option) => option.value === this.value
|
||||
);
|
||||
if (!selectedItem) {
|
||||
if (selectedItemIndex === -1) {
|
||||
this.value = undefined;
|
||||
}
|
||||
if (localeChanged) {
|
||||
this._select.select(selectedItemIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _getLanguagesOptions = memoizeOne(
|
||||
(languages: string[], locale: FrontendLocaleData, nativeName: boolean) => {
|
||||
(languages: string[], nativeName: boolean, locale?: FrontendLocaleData) => {
|
||||
let options: { label: string; value: string }[] = [];
|
||||
|
||||
if (nativeName) {
|
||||
const translations = this.hass.translationMetadata.translations;
|
||||
const translations = translationMetadata.translations;
|
||||
options = languages.map((lang) => {
|
||||
let label = translations[lang]?.nativeName;
|
||||
if (!label) {
|
||||
@@ -87,14 +102,14 @@ export class HaLanguagePicker extends LitElement {
|
||||
label,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
} else if (locale) {
|
||||
options = languages.map((lang) => ({
|
||||
value: lang,
|
||||
label: formatLanguageCode(lang, locale),
|
||||
}));
|
||||
}
|
||||
|
||||
if (!this.noSort) {
|
||||
if (!this.noSort && locale) {
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.label, b.label, locale.language)
|
||||
);
|
||||
@@ -104,20 +119,14 @@ export class HaLanguagePicker extends LitElement {
|
||||
);
|
||||
|
||||
private _computeDefaultLanguageOptions() {
|
||||
if (!this.hass.translationMetadata?.translations) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._defaultLanguages = Object.keys(
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
this._defaultLanguages = Object.keys(translationMetadata.translations);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const languageOptions = this._getLanguagesOptions(
|
||||
this.languages ?? this._defaultLanguages,
|
||||
this.hass.locale,
|
||||
this.nativeName
|
||||
this.nativeName,
|
||||
this.hass?.locale
|
||||
);
|
||||
|
||||
const value =
|
||||
@@ -125,9 +134,10 @@ export class HaLanguagePicker extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.language-picker.language")}
|
||||
.value=${value}
|
||||
.label=${this.label ??
|
||||
(this.hass?.localize("ui.components.language-picker.language") ||
|
||||
"Language")}
|
||||
.value=${value || ""}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@@ -137,9 +147,9 @@ export class HaLanguagePicker extends LitElement {
|
||||
>
|
||||
${languageOptions.length === 0
|
||||
? html`<ha-list-item value=""
|
||||
>${this.hass.localize(
|
||||
>${this.hass?.localize(
|
||||
"ui.components.language-picker.no_languages"
|
||||
)}</ha-list-item
|
||||
) || "No languages"}</ha-list-item
|
||||
>`
|
||||
: languageOptions.map(
|
||||
(option) => html`
|
||||
@@ -162,7 +172,7 @@ export class HaLanguagePicker extends LitElement {
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (!this.hass || target.value === "" || target.value === this.value) {
|
||||
if (target.value === "" || target.value === this.value) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value;
|
||||
|
91
src/components/ha-lawn_mower-action-button.ts
Normal file
91
src/components/ha-lawn_mower-action-button.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import "@material/mwc-button";
|
||||
import { CSSResultGroup, LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import {
|
||||
LawnMowerEntity,
|
||||
LawnMowerEntityFeature,
|
||||
LawnMowerEntityState,
|
||||
} from "../data/lawn_mower";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
type LawnMowerAction = {
|
||||
action: string;
|
||||
service: string;
|
||||
feature: LawnMowerEntityFeature;
|
||||
};
|
||||
|
||||
const LAWN_MOWER_ACTIONS: Partial<
|
||||
Record<LawnMowerEntityState, LawnMowerAction>
|
||||
> = {
|
||||
mowing: {
|
||||
action: "dock",
|
||||
service: "dock",
|
||||
feature: LawnMowerEntityFeature.DOCK,
|
||||
},
|
||||
docked: {
|
||||
action: "start_mowing",
|
||||
service: "start_mowing",
|
||||
feature: LawnMowerEntityFeature.START_MOWING,
|
||||
},
|
||||
paused: {
|
||||
action: "resume_mowing",
|
||||
service: "start_mowing",
|
||||
feature: LawnMowerEntityFeature.START_MOWING,
|
||||
},
|
||||
};
|
||||
|
||||
@customElement("ha-lawn_mower-action-button")
|
||||
class HaLawnMowerActionButton extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: LawnMowerEntity;
|
||||
|
||||
public render() {
|
||||
const state = this.stateObj.state;
|
||||
const action = LAWN_MOWER_ACTIONS[state];
|
||||
|
||||
if (action && supportsFeature(this.stateObj, action.feature)) {
|
||||
return html`
|
||||
<mwc-button @click=${this.callService} .service=${action.service}>
|
||||
${this.hass.localize(`ui.card.lawn_mower.actions.${action.action}`)}
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<mwc-button disabled>
|
||||
${this.hass.formatEntityState(this.stateObj)}
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
callService(ev) {
|
||||
ev.stopPropagation();
|
||||
const stateObj = this.stateObj;
|
||||
const service = ev.target.service;
|
||||
this.hass.callService("lawn_mower", service, {
|
||||
entity_id: stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
mwc-button {
|
||||
top: 3px;
|
||||
height: 37px;
|
||||
margin-right: -0.57em;
|
||||
}
|
||||
mwc-button[disabled] {
|
||||
background-color: transparent;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-lawn_mower-action-button": HaLawnMowerActionButton;
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { mdiImagePlus } from "@mdi/js";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
showImageCropperDialog,
|
||||
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-button";
|
||||
import "./ha-circular-progress";
|
||||
import "./ha-file-upload";
|
||||
|
||||
@@ -20,6 +21,12 @@ export class HaPictureUpload extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public secondary?: string;
|
||||
|
||||
@property() public supports?: string;
|
||||
|
||||
@property() public currentImageAltText?: string;
|
||||
|
||||
@property({ type: Boolean }) public crop = false;
|
||||
|
||||
@property({ attribute: false }) public cropOptions?: CropOptions;
|
||||
@@ -29,19 +36,44 @@ export class HaPictureUpload extends LitElement {
|
||||
@state() private _uploading = false;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.icon=${mdiImagePlus}
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.picture-upload.label")}
|
||||
.uploading=${this._uploading}
|
||||
.value=${this.value ? html`<img .src=${this.value} />` : ""}
|
||||
@file-picked=${this._handleFilePicked}
|
||||
@change=${this._handleFileCleared}
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
></ha-file-upload>
|
||||
`;
|
||||
if (!this.value) {
|
||||
return html`
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.icon=${mdiImagePlus}
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.picture-upload.label")}
|
||||
.secondary=${this.secondary}
|
||||
.supports=${this.supports ||
|
||||
this.hass.localize("ui.components.picture-upload.supported_formats")}
|
||||
.uploading=${this._uploading}
|
||||
@file-picked=${this._handleFilePicked}
|
||||
@change=${this._handleFileCleared}
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
></ha-file-upload>
|
||||
`;
|
||||
}
|
||||
return html`<div class="center-vertical">
|
||||
<div class="value">
|
||||
<img
|
||||
.src=${this.value}
|
||||
alt=${this.currentImageAltText ||
|
||||
this.hass.localize("ui.components.picture-upload.current_image_alt")}
|
||||
/>
|
||||
<ha-button
|
||||
@click=${this._handleChangeClick}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.picture-upload.change_picture"
|
||||
)}
|
||||
>
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _handleChangeClick() {
|
||||
this.value = null;
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
private async _handleFilePicked(ev) {
|
||||
@@ -100,6 +132,35 @@ export class HaPictureUpload extends LitElement {
|
||||
this._uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 240px;
|
||||
}
|
||||
ha-file-upload {
|
||||
height: 100%;
|
||||
}
|
||||
.center-vertical {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -47,6 +47,9 @@ export class HaSelect extends SelectBase {
|
||||
.mdc-select__anchor {
|
||||
width: var(--ha-select-min-width, 200px);
|
||||
}
|
||||
.mdc-select--filled .mdc-select__anchor {
|
||||
height: var(--ha-select-height, 56px);
|
||||
}
|
||||
.mdc-select--filled .mdc-floating-label {
|
||||
inset-inline-start: 12px;
|
||||
inset-inline-end: initial;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { Action } from "../../data/script";
|
||||
import { ActionSelector } from "../../data/selector";
|
||||
@@ -19,10 +19,13 @@ export class HaActionSelector extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-automation-action
|
||||
.disabled=${this.disabled}
|
||||
.actions=${this.value || []}
|
||||
.hass=${this.hass}
|
||||
.nested=${this.selector.action?.nested}
|
||||
.reOrderMode=${this.selector.action?.reorder_mode}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
@@ -37,6 +40,11 @@ export class HaActionSelector extends LitElement {
|
||||
opacity: var(--light-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { Condition } from "../../data/automation";
|
||||
import { ConditionSelector } from "../../data/selector";
|
||||
@@ -19,10 +19,13 @@ export class HaConditionSelector extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-automation-condition
|
||||
.disabled=${this.disabled}
|
||||
.conditions=${this.value || []}
|
||||
.hass=${this.hass}
|
||||
.nested=${this.selector.condition?.nested}
|
||||
.reOrderMode=${this.selector.condition?.reorder_mode}
|
||||
></ha-automation-condition>
|
||||
`;
|
||||
}
|
||||
@@ -37,6 +40,11 @@ export class HaConditionSelector extends LitElement {
|
||||
opacity: var(--light-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -37,9 +37,12 @@ export class HaFileSelector extends LitElement {
|
||||
.label=${this.label}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
.supports=${this.helper}
|
||||
.uploading=${this._busy}
|
||||
.value=${this.value ? this._filename?.name || "Unknown file" : ""}
|
||||
.value=${this.value
|
||||
? this._filename?.name ||
|
||||
this.hass.localize("ui.components.selectors.file.unknown_file")
|
||||
: undefined}
|
||||
@file-picked=${this._uploadFile}
|
||||
@change=${this._removeFile}
|
||||
></ha-file-upload>
|
||||
|
@@ -1075,7 +1075,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
background-color: var(--accent-color);
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
padding: 0px 6px;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
ha-svg-icon + .notification-badge,
|
||||
|
@@ -65,6 +65,7 @@ export class HaTabs extends PaperTabs {
|
||||
const selected = this.querySelector(".iron-selected");
|
||||
if (selected) {
|
||||
selected.scrollIntoView();
|
||||
this._affectScroll(0); // Ensure scroll arrows match scroll position
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,436 +0,0 @@
|
||||
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
|
||||
import { css, html, LitElement, PropertyValues, svg } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { rgb2hex } from "../common/color/convert-color";
|
||||
import { temperature2rgb } from "../common/color/convert-light-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
const SAFE_ZONE_FACTOR = 0.9;
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"cursor-moved": { value?: any };
|
||||
}
|
||||
}
|
||||
|
||||
const A11Y_KEY_CODES = new Set([
|
||||
"ArrowRight",
|
||||
"ArrowUp",
|
||||
"ArrowLeft",
|
||||
"ArrowDown",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"Home",
|
||||
"End",
|
||||
]);
|
||||
|
||||
function xy2polar(x: number, y: number) {
|
||||
const r = Math.sqrt(x * x + y * y);
|
||||
const phi = Math.atan2(y, x);
|
||||
return [r, phi];
|
||||
}
|
||||
|
||||
function polar2xy(r: number, phi: number) {
|
||||
const x = Math.cos(phi) * r;
|
||||
const y = Math.sin(phi) * r;
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
function drawColorWheel(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
minTemp: number,
|
||||
maxTemp: number
|
||||
) {
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
const radius = ctx.canvas.width / 2;
|
||||
|
||||
const min = Math.max(minTemp, 2000);
|
||||
const max = Math.min(maxTemp, 40000);
|
||||
|
||||
for (let y = -radius; y < radius; y += 1) {
|
||||
const x = radius * Math.sqrt(1 - (y / radius) ** 2);
|
||||
|
||||
const fraction = (y / (radius * SAFE_ZONE_FACTOR) + 1) / 2;
|
||||
|
||||
const temperature = Math.max(
|
||||
Math.min(min + fraction * (max - min), max),
|
||||
min
|
||||
);
|
||||
|
||||
const color = rgb2hex(temperature2rgb(temperature));
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(radius - x, radius + y - 0.5, 2 * x, 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-temp-color-picker")
|
||||
class HaTempColorPicker extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Number, attribute: false })
|
||||
public renderSize?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public value?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public min = 2000;
|
||||
|
||||
@property({ type: Number })
|
||||
public max = 10000;
|
||||
|
||||
@query("#canvas") private _canvas!: HTMLCanvasElement;
|
||||
|
||||
private _mc?: HammerManager;
|
||||
|
||||
@state()
|
||||
private _pressed?: string;
|
||||
|
||||
@state()
|
||||
private _cursorPosition?: [number, number];
|
||||
|
||||
@state()
|
||||
private _localValue?: number;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this._setupListeners();
|
||||
this._generateColorWheel();
|
||||
this.setAttribute("role", "slider");
|
||||
this.setAttribute("aria-orientation", "vertical");
|
||||
if (!this.hasAttribute("tabindex")) {
|
||||
this.setAttribute("tabindex", "0");
|
||||
}
|
||||
}
|
||||
|
||||
private _generateColorWheel() {
|
||||
const ctx = this._canvas.getContext("2d")!;
|
||||
drawColorWheel(ctx, this.min, this.max);
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._setupListeners();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._destroyListeners();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("_localValue")) {
|
||||
this.setAttribute("aria-valuenow", this._localValue?.toString() ?? "");
|
||||
}
|
||||
if (changedProps.has("min") || changedProps.has("max")) {
|
||||
this._generateColorWheel();
|
||||
this._resetPosition();
|
||||
}
|
||||
if (changedProps.has("min")) {
|
||||
this.setAttribute("aria-valuemin", this.min.toString());
|
||||
}
|
||||
if (changedProps.has("max")) {
|
||||
this.setAttribute("aria-valuemax", this.max.toString());
|
||||
}
|
||||
if (changedProps.has("value")) {
|
||||
if (this._localValue !== this.value) {
|
||||
this._resetPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _setupListeners() {
|
||||
if (this._canvas && !this._mc) {
|
||||
this._mc = new Manager(this._canvas);
|
||||
this._mc.add(
|
||||
new Pan({
|
||||
direction: DIRECTION_ALL,
|
||||
enable: true,
|
||||
threshold: 0,
|
||||
})
|
||||
);
|
||||
|
||||
this._mc.add(new Tap({ event: "singletap" }));
|
||||
|
||||
let savedPosition;
|
||||
this._mc.on("panstart", (e) => {
|
||||
if (this.disabled) return;
|
||||
this._pressed = e.pointerType;
|
||||
savedPosition = this._cursorPosition;
|
||||
});
|
||||
this._mc.on("pancancel", () => {
|
||||
if (this.disabled) return;
|
||||
this._pressed = undefined;
|
||||
this._cursorPosition = savedPosition;
|
||||
});
|
||||
this._mc.on("panmove", (e) => {
|
||||
if (this.disabled) return;
|
||||
this._cursorPosition = this._getPositionFromEvent(e);
|
||||
this._localValue = this._getValueFromCoord(...this._cursorPosition);
|
||||
fireEvent(this, "cursor-moved", { value: this._localValue });
|
||||
});
|
||||
this._mc.on("panend", (e) => {
|
||||
if (this.disabled) return;
|
||||
this._pressed = undefined;
|
||||
this._cursorPosition = this._getPositionFromEvent(e);
|
||||
this._localValue = this._getValueFromCoord(...this._cursorPosition);
|
||||
fireEvent(this, "cursor-moved", { value: undefined });
|
||||
fireEvent(this, "value-changed", { value: this._localValue });
|
||||
});
|
||||
|
||||
this._mc.on("singletap", (e) => {
|
||||
if (this.disabled) return;
|
||||
this._cursorPosition = this._getPositionFromEvent(e);
|
||||
this._localValue = this._getValueFromCoord(...this._cursorPosition);
|
||||
fireEvent(this, "value-changed", { value: this._localValue });
|
||||
});
|
||||
|
||||
this.addEventListener("keydown", this._handleKeyDown);
|
||||
this.addEventListener("keyup", this._handleKeyUp);
|
||||
}
|
||||
}
|
||||
|
||||
private _resetPosition() {
|
||||
if (this.value === undefined) {
|
||||
this._cursorPosition = undefined;
|
||||
this._localValue = undefined;
|
||||
return;
|
||||
}
|
||||
const [, y] = this._getCoordsFromValue(this.value);
|
||||
const currentX = this._cursorPosition?.[0] ?? 0;
|
||||
const x =
|
||||
Math.sign(currentX) * Math.min(Math.sqrt(1 - y ** 2), Math.abs(currentX));
|
||||
this._cursorPosition = [x, y];
|
||||
this._localValue = this.value;
|
||||
}
|
||||
|
||||
private _getCoordsFromValue = (temperature: number): [number, number] => {
|
||||
if (this.value === this.min) {
|
||||
return [0, -1];
|
||||
}
|
||||
if (this.value === this.max) {
|
||||
return [0, 1];
|
||||
}
|
||||
const fraction = (temperature - this.min) / (this.max - this.min);
|
||||
const y = (2 * fraction - 1) * SAFE_ZONE_FACTOR;
|
||||
return [0, y];
|
||||
};
|
||||
|
||||
private _getValueFromCoord = (_x: number, y: number): number => {
|
||||
const fraction = (y / SAFE_ZONE_FACTOR + 1) / 2;
|
||||
const temperature = Math.max(
|
||||
Math.min(this.min + fraction * (this.max - this.min), this.max),
|
||||
this.min
|
||||
);
|
||||
return Math.round(temperature);
|
||||
};
|
||||
|
||||
private _getPositionFromEvent = (e: HammerInput): [number, number] => {
|
||||
const x = e.center.x;
|
||||
const y = e.center.y;
|
||||
const boundingRect = e.target.getBoundingClientRect();
|
||||
const offsetX = boundingRect.left;
|
||||
const offsetY = boundingRect.top;
|
||||
const maxX = e.target.clientWidth;
|
||||
const maxY = e.target.clientHeight;
|
||||
|
||||
const _x = (2 * (x - offsetX)) / maxX - 1;
|
||||
const _y = (2 * (y - offsetY)) / maxY - 1;
|
||||
|
||||
const [r, phi] = xy2polar(_x, _y);
|
||||
const [__x, __y] = polar2xy(Math.min(1, r), phi);
|
||||
return [__x, __y];
|
||||
};
|
||||
|
||||
private _destroyListeners() {
|
||||
if (this._mc) {
|
||||
this._mc.destroy();
|
||||
this._mc = undefined;
|
||||
}
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
this.removeEventListener("keyup", this._handleKeyDown);
|
||||
}
|
||||
|
||||
_handleKeyDown(e: KeyboardEvent) {
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
e.preventDefault();
|
||||
|
||||
const step = 1;
|
||||
const tenPercentStep = Math.max(step, (this.max - this.min) / 10);
|
||||
const currentValue =
|
||||
this._localValue ?? Math.round((this.max + this.min) / 2);
|
||||
switch (e.code) {
|
||||
case "ArrowRight":
|
||||
case "ArrowUp":
|
||||
this._localValue = Math.round(Math.min(currentValue + step, this.max));
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
case "ArrowDown":
|
||||
this._localValue = Math.round(Math.max(currentValue - step, this.min));
|
||||
break;
|
||||
case "PageUp":
|
||||
this._localValue = Math.round(
|
||||
Math.min(currentValue + tenPercentStep, this.max)
|
||||
);
|
||||
break;
|
||||
case "PageDown":
|
||||
this._localValue = Math.round(
|
||||
Math.max(currentValue - tenPercentStep, this.min)
|
||||
);
|
||||
break;
|
||||
case "Home":
|
||||
this._localValue = this.min;
|
||||
break;
|
||||
case "End":
|
||||
this._localValue = this.max;
|
||||
break;
|
||||
}
|
||||
if (this._localValue != null) {
|
||||
const [_, y] = this._getCoordsFromValue(this._localValue);
|
||||
const currentX = this._cursorPosition?.[0] ?? 0;
|
||||
const x =
|
||||
Math.sign(currentX) *
|
||||
Math.min(Math.sqrt(1 - y ** 2), Math.abs(currentX));
|
||||
this._cursorPosition = [x, y];
|
||||
fireEvent(this, "cursor-moved", { value: this._localValue });
|
||||
}
|
||||
}
|
||||
|
||||
_handleKeyUp(e: KeyboardEvent) {
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
e.preventDefault();
|
||||
this.value = this._localValue;
|
||||
fireEvent(this, "value-changed", { value: this._localValue });
|
||||
}
|
||||
|
||||
render() {
|
||||
const size = this.renderSize || 400;
|
||||
const canvasSize = size * window.devicePixelRatio;
|
||||
|
||||
const rgb = temperature2rgb(
|
||||
this._localValue ?? Math.round((this.max + this.min) / 2)
|
||||
);
|
||||
|
||||
const [x, y] = this._cursorPosition ?? [0, 0];
|
||||
|
||||
const cx = ((x + 1) * size) / 2;
|
||||
const cy = ((y + 1) * size) / 2;
|
||||
|
||||
const markerPosition = `${cx}px, ${cy}px`;
|
||||
const markerScale = this._pressed
|
||||
? this._pressed === "touch"
|
||||
? "2.5"
|
||||
: "1.5"
|
||||
: "1";
|
||||
const markerOffset =
|
||||
this._pressed === "touch" ? `0px, -${size / 16}px` : "0px, 0px";
|
||||
|
||||
return html`
|
||||
<div class="container ${classMap({ pressed: Boolean(this._pressed) })}">
|
||||
<canvas id="canvas" .width=${canvasSize} .height=${canvasSize}></canvas>
|
||||
<svg
|
||||
id="interaction"
|
||||
viewBox="0 0 ${size} ${size}"
|
||||
overflow="visible"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>${this.renderSVGFilter()}</defs>
|
||||
<g
|
||||
style=${styleMap({
|
||||
fill: rgb2hex(rgb),
|
||||
transform: `translate(${markerPosition})`,
|
||||
})}
|
||||
class="cursor"
|
||||
>
|
||||
<circle
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="16"
|
||||
style=${styleMap({
|
||||
fill: rgb2hex(rgb),
|
||||
transform: `translate(${markerOffset}) scale(${markerScale})`,
|
||||
visibility: this._cursorPosition ? undefined : "hidden",
|
||||
})}
|
||||
></circle>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderSVGFilter() {
|
||||
return svg`
|
||||
<filter
|
||||
id="marker-shadow"
|
||||
x="-50%"
|
||||
y="-50%"
|
||||
width="200%"
|
||||
height="200%"
|
||||
filterUnits="objectBoundingBox"
|
||||
>
|
||||
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.3" flood-color="rgba(0, 0, 0, 1)"/>
|
||||
<feDropShadow dx="0" dy="1" stdDeviation="3" flood-opacity="0.15" flood-color="rgba(0, 0, 0, 1)"/>
|
||||
</filter>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
outline: none;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 50%;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
:host(:focus-visible) canvas {
|
||||
box-shadow: 0 0 0 2px rgb(255, 160, 0);
|
||||
}
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
circle {
|
||||
fill: black;
|
||||
stroke: white;
|
||||
stroke-width: 2;
|
||||
filter: url(#marker-shadow);
|
||||
}
|
||||
.container:not(.pressed) circle {
|
||||
transition:
|
||||
transform 100ms ease-in-out,
|
||||
fill 100ms ease-in-out;
|
||||
}
|
||||
.container:not(.pressed) .cursor {
|
||||
transition: transform 200ms ease-in-out;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-temp-color-picker": HaTempColorPicker;
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import LocalizeMixin from "../mixins/localize-mixin";
|
||||
|
||||
@@ -84,12 +83,7 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
_localizeState(stateObj) {
|
||||
return computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
);
|
||||
return this.hass.formatEntityState(stateObj);
|
||||
}
|
||||
}
|
||||
customElements.define("ha-water_heater-state", HaWaterHeaterState);
|
||||
|
@@ -102,9 +102,8 @@ class HaWebRtcPlayer extends LitElement {
|
||||
offerToReceiveAudio: true,
|
||||
offerToReceiveVideo: true,
|
||||
};
|
||||
const offer: RTCSessionDescriptionInit = await peerConnection.createOffer(
|
||||
offerOptions
|
||||
);
|
||||
const offer: RTCSessionDescriptionInit =
|
||||
await peerConnection.createOffer(offerOptions);
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
|
||||
let candidates = ""; // Build an Offer SDP string with ice candidates
|
||||
|
@@ -1,15 +0,0 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const createLanguageListEl = (hass: HomeAssistant) => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "languages";
|
||||
for (const [language, metadata] of Object.entries(
|
||||
hass.translationMetadata.translations
|
||||
)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = language;
|
||||
option.innerText = metadata.nativeName;
|
||||
list.appendChild(option);
|
||||
}
|
||||
return list;
|
||||
};
|
@@ -122,6 +122,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user