mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
20231025.0 (#18395)
This commit is contained in:
commit
fdaefadd18
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@ -57,7 +57,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
20
.github/workflows/ci.yaml
vendored
20
.github/workflows/ci.yaml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
@ -55,7 +55,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
@ -73,7 +73,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
@ -85,13 +85,19 @@ jobs:
|
||||
run: ./node_modules/.bin/gulp build-app
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
supervisor:
|
||||
name: Build supervisor
|
||||
needs: [lint, test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
@ -103,3 +109,9 @@ jobs:
|
||||
run: ./node_modules/.bin/gulp build-hassio
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
|
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@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@ -58,7 +58,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@ -21,7 +21,7 @@ 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@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
|
6
.github/workflows/nightly.yaml
vendored
6
.github/workflows/nightly.yaml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v4
|
||||
@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
25
.github/workflows/relative-ci.yaml
vendored
Normal file
25
.github/workflows/relative-ci.yaml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: RelativeCI
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [CI]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
name: Upload stats
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
strategy:
|
||||
matrix:
|
||||
bundle: [frontend, supervisor]
|
||||
build: [modern, legacy]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@v2.1.10
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
artifactName: ${{ format('{0}-bundle-stats', matrix.bundle) }}
|
||||
webpackStatsFile: ${{ format('{0}-{1}.json', matrix.bundle, matrix.build) }}
|
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@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
@ -74,7 +74,7 @@ jobs:
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2023.10.1
|
||||
uses: home-assistant/wheels@2023.10.5
|
||||
with:
|
||||
abi: cp311
|
||||
tag: musllinux_1_2
|
||||
|
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@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -47,3 +47,6 @@ src/cast/dev_const.ts
|
||||
|
||||
# Home Assistant config
|
||||
/config/
|
||||
|
||||
# Jetbrains
|
||||
/.idea/
|
||||
|
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.3.cjs
|
||||
yarnPath: .yarn/releases/yarn-3.6.4.cjs
|
||||
|
@ -98,7 +98,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
"@babel/preset-env",
|
||||
{
|
||||
useBuiltIns: latestBuild ? false : "entry",
|
||||
corejs: latestBuild ? false : { version: "3.32", proposals: true },
|
||||
corejs: latestBuild ? false : { version: "3.33", proposals: true },
|
||||
bugfixes: true,
|
||||
shippedProposals: true,
|
||||
},
|
||||
@ -149,7 +149,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
sourceMaps: !isTestBuild,
|
||||
});
|
||||
|
||||
const nameSuffix = (latestBuild) => (latestBuild ? "-latest" : "-es5");
|
||||
const nameSuffix = (latestBuild) => (latestBuild ? "-modern" : "-legacy");
|
||||
|
||||
const outputPath = (outputRoot, latestBuild) =>
|
||||
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
|
||||
@ -183,7 +183,7 @@ const publicPath = (latestBuild, root = "") =>
|
||||
module.exports.config = {
|
||||
app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) {
|
||||
return {
|
||||
name: "app" + nameSuffix(latestBuild),
|
||||
name: "frontend" + nameSuffix(latestBuild),
|
||||
entry: {
|
||||
service_worker: "./src/entrypoints/service_worker.ts",
|
||||
app: "./src/entrypoints/app.ts",
|
||||
|
@ -4,6 +4,7 @@ import fs from "fs-extra";
|
||||
import gulp from "gulp";
|
||||
import path from "path";
|
||||
import paths from "../paths.cjs";
|
||||
import env from "../env.cjs";
|
||||
|
||||
const npmPath = (...parts) =>
|
||||
path.resolve(paths.polymer_dir, "node_modules", ...parts);
|
||||
@ -62,6 +63,9 @@ function copyPolyfills(staticDir) {
|
||||
}
|
||||
|
||||
function copyLoaderJS(staticDir) {
|
||||
if (!env.useRollup()) {
|
||||
return;
|
||||
}
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
copyFileDir(npmPath("systemjs/dist/s.min.js"), staticPath("js"));
|
||||
copyFileDir(npmPath("systemjs/dist/s.min.js.map"), staticPath("js"));
|
||||
|
@ -1,51 +1,54 @@
|
||||
import { deleteSync } from "del";
|
||||
import { mkdir, readFile, writeFile } from "fs/promises";
|
||||
import gulp from "gulp";
|
||||
import path from "path";
|
||||
import { join, resolve } from "node:path";
|
||||
import paths from "../paths.cjs";
|
||||
|
||||
const outDir = path.join(paths.build_dir, "locale-data");
|
||||
const formatjsDir = join(paths.polymer_dir, "node_modules", "@formatjs");
|
||||
const outDir = join(paths.build_dir, "locale-data");
|
||||
|
||||
const INTL_PACKAGES = {
|
||||
"intl-relativetimeformat": "RelativeTimeFormat",
|
||||
const INTL_POLYFILLS = {
|
||||
"intl-datetimeformat": "DateTimeFormat",
|
||||
"intl-numberformat": "NumberFormat",
|
||||
"intl-displaynames": "DisplayNames",
|
||||
"intl-listformat": "ListFormat",
|
||||
"intl-numberformat": "NumberFormat",
|
||||
"intl-relativetimeformat": "RelativeTimeFormat",
|
||||
};
|
||||
|
||||
const convertToJSON = async (pkg, lang) => {
|
||||
const convertToJSON = async (
|
||||
pkg,
|
||||
lang,
|
||||
subDir = "locale-data",
|
||||
addFunc = "__addLocaleData",
|
||||
skipMissing = true
|
||||
) => {
|
||||
let localeData;
|
||||
try {
|
||||
localeData = await readFile(
|
||||
path.resolve(
|
||||
paths.polymer_dir,
|
||||
`node_modules/@formatjs/${pkg}/locale-data/${lang}.js`
|
||||
),
|
||||
join(formatjsDir, pkg, subDir, `${lang}.js`),
|
||||
"utf-8"
|
||||
);
|
||||
} catch (e) {
|
||||
// Ignore if language is missing (i.e. not supported by @formatjs)
|
||||
if (e.code === "ENOENT") {
|
||||
if (e.code === "ENOENT" && skipMissing) {
|
||||
console.warn(`Skipped missing data for language ${lang} from ${pkg}`);
|
||||
return;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
// Convert to JSON
|
||||
const className = INTL_PACKAGES[pkg];
|
||||
localeData = localeData
|
||||
.replace(
|
||||
new RegExp(
|
||||
`\\/\\*\\s*@generated\\s*\\*\\/\\s*\\/\\/\\s*prettier-ignore\\s*if\\s*\\(Intl\\.${className}\\s*&&\\s*typeof\\s*Intl\\.${className}\\.__addLocaleData\\s*===\\s*'function'\\)\\s*{\\s*Intl\\.${className}\\.__addLocaleData\\(`,
|
||||
"im"
|
||||
),
|
||||
""
|
||||
)
|
||||
.replace(/\)\s*}/im, "");
|
||||
const obj = INTL_POLYFILLS[pkg];
|
||||
const dataRegex = new RegExp(
|
||||
`Intl\\.${obj}\\.${addFunc}\\((?<data>.*)\\)`,
|
||||
"s"
|
||||
);
|
||||
localeData = localeData.match(dataRegex)?.groups?.data;
|
||||
if (!localeData) {
|
||||
throw Error(`Failed to extract data for language ${lang} from ${pkg}`);
|
||||
}
|
||||
// Parse to validate JSON, then stringify to minify
|
||||
localeData = JSON.stringify(JSON.parse(localeData));
|
||||
await writeFile(path.join(outDir, `${pkg}/${lang}.json`), localeData);
|
||||
await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData);
|
||||
};
|
||||
|
||||
gulp.task("clean-locale-data", async () => deleteSync([outDir]));
|
||||
@ -53,17 +56,27 @@ gulp.task("clean-locale-data", async () => deleteSync([outDir]));
|
||||
gulp.task("create-locale-data", async () => {
|
||||
const translationMeta = JSON.parse(
|
||||
await readFile(
|
||||
path.resolve(paths.translations_src, "translationMetadata.json"),
|
||||
resolve(paths.translations_src, "translationMetadata.json"),
|
||||
"utf-8"
|
||||
)
|
||||
);
|
||||
const conversions = [];
|
||||
for (const pkg of Object.keys(INTL_PACKAGES)) {
|
||||
await mkdir(path.join(outDir, pkg), { recursive: true });
|
||||
for (const pkg of Object.keys(INTL_POLYFILLS)) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await mkdir(join(outDir, pkg), { recursive: true });
|
||||
for (const lang of Object.keys(translationMeta)) {
|
||||
conversions.push(convertToJSON(pkg, lang));
|
||||
}
|
||||
}
|
||||
conversions.push(
|
||||
convertToJSON(
|
||||
"intl-datetimeformat",
|
||||
"add-all-tz",
|
||||
".",
|
||||
"__addTZData",
|
||||
false
|
||||
)
|
||||
);
|
||||
await Promise.all(conversions);
|
||||
});
|
||||
|
||||
|
@ -1,12 +1,7 @@
|
||||
import { createHash } from "crypto";
|
||||
import { deleteSync } from "del";
|
||||
import {
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
writeFile,
|
||||
} from "fs";
|
||||
import { mkdirSync, readdirSync, readFileSync, renameSync } from "fs";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import gulp from "gulp";
|
||||
import flatmap from "gulp-flatmap";
|
||||
import transform from "gulp-json-transform";
|
||||
@ -136,27 +131,23 @@ gulp.task("ensure-translations-build-dir", async () => {
|
||||
mkdirSync(workDir, { recursive: true });
|
||||
});
|
||||
|
||||
gulp.task("create-test-metadata", (cb) => {
|
||||
writeFile(
|
||||
workDir + "/testMetadata.json",
|
||||
JSON.stringify({
|
||||
test: {
|
||||
nativeName: "Test",
|
||||
},
|
||||
}),
|
||||
cb
|
||||
);
|
||||
});
|
||||
gulp.task("create-test-metadata", () =>
|
||||
env.isProdBuild()
|
||||
? Promise.resolve()
|
||||
: writeFile(
|
||||
workDir + "/testMetadata.json",
|
||||
JSON.stringify({ test: { nativeName: "Test" } })
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"create-test-translation",
|
||||
gulp.series("create-test-metadata", () =>
|
||||
gulp
|
||||
.src(path.join(paths.translations_src, "en.json"))
|
||||
.pipe(transform((data, _file) => recursiveEmpty(data)))
|
||||
.pipe(rename("test.json"))
|
||||
.pipe(gulp.dest(workDir))
|
||||
)
|
||||
gulp.task("create-test-translation", () =>
|
||||
env.isProdBuild()
|
||||
? Promise.resolve()
|
||||
: gulp
|
||||
.src(path.join(paths.translations_src, "en.json"))
|
||||
.pipe(transform((data, _file) => recursiveEmpty(data)))
|
||||
.pipe(rename("test.json"))
|
||||
.pipe(gulp.dest(workDir))
|
||||
);
|
||||
|
||||
/**
|
||||
@ -188,16 +179,11 @@ gulp.task("build-master-translation", () => {
|
||||
|
||||
gulp.task("build-merged-translations", () =>
|
||||
gulp
|
||||
.src(
|
||||
[
|
||||
inFrontendDir + "/*.json",
|
||||
"!" + inFrontendDir + "/en.json",
|
||||
workDir + "/test.json",
|
||||
],
|
||||
{
|
||||
allowEmpty: true,
|
||||
}
|
||||
)
|
||||
.src([
|
||||
inFrontendDir + "/*.json",
|
||||
"!" + inFrontendDir + "/en.json",
|
||||
...(env.isProdBuild() ? [] : [workDir + "/test.json"]),
|
||||
])
|
||||
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
|
||||
.pipe(
|
||||
flatmap((stream, file) => {
|
||||
@ -377,14 +363,11 @@ gulp.task("build-translation-flatten-supervisor", () =>
|
||||
|
||||
gulp.task("build-translation-write-metadata", () =>
|
||||
gulp
|
||||
.src(
|
||||
[
|
||||
path.join(paths.translations_src, "translationMetadata.json"),
|
||||
workDir + "/testMetadata.json",
|
||||
workDir + "/translationFingerprints.json",
|
||||
],
|
||||
{ allowEmpty: true }
|
||||
)
|
||||
.src([
|
||||
path.join(paths.translations_src, "translationMetadata.json"),
|
||||
...(env.isProdBuild() ? [] : [workDir + "/testMetadata.json"]),
|
||||
workDir + "/translationFingerprints.json",
|
||||
])
|
||||
.pipe(merge({}))
|
||||
.pipe(
|
||||
transform((data) => {
|
||||
@ -415,7 +398,7 @@ gulp.task("build-translation-write-metadata", () =>
|
||||
gulp.task(
|
||||
"create-translations",
|
||||
gulp.series(
|
||||
...(env.isProdBuild() ? [] : ["create-test-translation"]),
|
||||
gulp.parallel("create-test-metadata", "create-test-translation"),
|
||||
"build-master-translation",
|
||||
"build-merged-translations",
|
||||
gulp.parallel(...splitTasks),
|
||||
|
@ -1,6 +1,8 @@
|
||||
const { existsSync } = require("fs");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { StatsWriterPlugin } = require("webpack-stats-plugin");
|
||||
const filterStats = require("@bundle-stats/plugin-webpack-filter").default;
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||
const log = require("fancy-log");
|
||||
@ -152,6 +154,15 @@ const createWebpackConfig = ({
|
||||
)
|
||||
),
|
||||
!isProdBuild && new LogStartCompilePlugin(),
|
||||
isProdBuild &&
|
||||
new StatsWriterPlugin({
|
||||
filename: path.relative(
|
||||
outputPath,
|
||||
path.join(paths.build_dir, "stats", `${name}.json`)
|
||||
),
|
||||
stats: { assets: true, chunks: true, modules: true },
|
||||
transform: (stats) => JSON.stringify(filterStats(stats)),
|
||||
}),
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
@ -171,6 +182,8 @@ const createWebpackConfig = ({
|
||||
"@lit-labs/virtualizer/layouts/grid.js",
|
||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
|
||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
||||
"@lit-labs/observers/resize-controller":
|
||||
"@lit-labs/observers/resize-controller.js",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
@ -183,6 +196,7 @@ const createWebpackConfig = ({
|
||||
isProdBuild && !isStatsBuild ? "[id]-[contenthash].js" : "[name].js",
|
||||
assetModuleFilename:
|
||||
isProdBuild && !isStatsBuild ? "[id]-[contenthash][ext]" : "[id][ext]",
|
||||
crossOriginLoading: "use-credentials",
|
||||
hashFunction: "xxhash64",
|
||||
hashDigest: "base64url",
|
||||
hashDigestLength: 11, // full length of 64 bit base64url
|
||||
|
@ -1,4 +1,4 @@
|
||||
import "../../../src/resources/safari-14-attachshadow-patch";
|
||||
import "../../../src/resources/ha-style";
|
||||
import "../../../src/resources/roboto";
|
||||
import "./layout/hc-connect";
|
||||
|
||||
import("../../../src/resources/ha-style");
|
||||
|
@ -100,7 +100,9 @@ export class HcMain extends HassElement {
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
import("../second-load");
|
||||
import("./hc-lovelace");
|
||||
import("../../../../src/resources/ha-style");
|
||||
|
||||
window.addEventListener("location-changed", () => {
|
||||
const panelPath = `/${this._urlPath || "lovelace"}/`;
|
||||
if (location.pathname.startsWith(panelPath)) {
|
||||
@ -260,7 +262,6 @@ export class HcMain extends HassElement {
|
||||
{
|
||||
strategy: {
|
||||
type: "energy",
|
||||
show_date_selection: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -308,7 +309,7 @@ export class HcMain extends HassElement {
|
||||
? await fetchResources(this.hass!.connection)
|
||||
: (this._lovelaceConfig as LegacyLovelaceConfig).resources;
|
||||
if (resources) {
|
||||
loadLovelaceResources(resources, this.hass!.auth.data.hassUrl);
|
||||
loadLovelaceResources(resources, this.hass!);
|
||||
}
|
||||
}
|
||||
|
||||
@ -324,8 +325,7 @@ export class HcMain extends HassElement {
|
||||
{
|
||||
type: DEFAULT_STRATEGY,
|
||||
},
|
||||
this.hass!,
|
||||
{ narrow: false }
|
||||
this.hass!
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
import "../../../src/resources/ha-style";
|
||||
import "../../../src/resources/roboto";
|
||||
import "./layout/hc-lovelace";
|
@ -3,6 +3,15 @@ import { DemoConfig } from "../types";
|
||||
|
||||
export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
convertEntities({
|
||||
"todo.shopping_list": {
|
||||
entity_id: "todo.shopping_list",
|
||||
state: "2",
|
||||
attributes: {
|
||||
supported_features: 15,
|
||||
friendly_name: "Shopping List",
|
||||
icon: "mdi:cart",
|
||||
},
|
||||
},
|
||||
"zone.home": {
|
||||
entity_id: "zone.home",
|
||||
state: "zoning",
|
||||
|
@ -3,6 +3,15 @@ import { DemoConfig } from "../types";
|
||||
|
||||
export const demoEntitiesJimpower: DemoConfig["entities"] = () =>
|
||||
convertEntities({
|
||||
"todo.shopping_list": {
|
||||
entity_id: "todo.shopping_list",
|
||||
state: "2",
|
||||
attributes: {
|
||||
supported_features: 15,
|
||||
friendly_name: "Shopping List",
|
||||
icon: "mdi:cart",
|
||||
},
|
||||
},
|
||||
"zone.powertec": {
|
||||
entity_id: "zone.powertec",
|
||||
state: "zoning",
|
||||
|
@ -4,16 +4,11 @@ export const demoThemeJimpower = () => ({
|
||||
"primary-color": "#5294E2",
|
||||
"label-badge-red": "var(--accent-color)",
|
||||
"paper-tabs-selection-bar-color": "green",
|
||||
"paper-slider-knob-color": "var(--accent-color)",
|
||||
"light-primary-color": "var(--accent-color)",
|
||||
"primary-background-color": "#383C45",
|
||||
"primary-text-color": "#FFFFFF",
|
||||
"paper-item-selected_-_background-color": "#434954",
|
||||
"paper-slider-active-color": "var(--accent-color)",
|
||||
"secondary-background-color": "#383C45",
|
||||
"paper-slider-container-color":
|
||||
"linear-gradient(var(--primary-background-color), var(--secondary-background-color)) no-repeat",
|
||||
"paper-slider-disabled-active-color": "var(--disabled-text-color)",
|
||||
"disabled-text-color": "#7F848E",
|
||||
"paper-item-icon_-_color": "green",
|
||||
"paper-grey-200": "#414A59",
|
||||
@ -32,14 +27,10 @@ export const demoThemeJimpower = () => ({
|
||||
"switch-unchecked-button-color": "var(--disabled-text-color)",
|
||||
"label-badge-border-color": "green",
|
||||
"paper-listbox-color": "var(--primary-color)",
|
||||
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
|
||||
"card-background-color": "#434954",
|
||||
"label-badge-text-color": "var(--primary-text-color)",
|
||||
"paper-slider-knob-start-color": "var(--accent-color)",
|
||||
"switch-unchecked-track-color": "var(--disabled-text-color)",
|
||||
"dark-primary-color": "var(--accent-color)",
|
||||
"paper-slider-secondary-color": "var(--secondary-background-color)",
|
||||
"paper-slider-pin-color": "var(--accent-color)",
|
||||
"paper-item-icon-active-color": "#F9C536",
|
||||
"accent-color": "#E45E65",
|
||||
"table-row-alternative-background-color": "#3E424B",
|
||||
|
@ -3,6 +3,15 @@ import { DemoConfig } from "../types";
|
||||
|
||||
export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
convertEntities({
|
||||
"todo.shopping_list": {
|
||||
entity_id: "todo.shopping_list",
|
||||
state: "2",
|
||||
attributes: {
|
||||
supported_features: 15,
|
||||
friendly_name: "Shopping List",
|
||||
icon: "mdi:cart",
|
||||
},
|
||||
},
|
||||
"zone.anna": {
|
||||
entity_id: "zone.anna",
|
||||
state: "zoning",
|
||||
|
@ -5,17 +5,12 @@ export const demoThemeKernehed = () => ({
|
||||
"primary-color": "#2980b9",
|
||||
"label-badge-red": "var(--accent-color)",
|
||||
"paper-tabs-selection-bar-color": "green",
|
||||
"paper-slider-knob-color": "var(--accent-color)",
|
||||
"primary-text-color": "#FFFFFF",
|
||||
"light-primary-color": "var(--accent-color)",
|
||||
"primary-background-color": "#222222",
|
||||
"sidebar-icon-color": "#777777",
|
||||
"paper-item-selected_-_background-color": "#292929",
|
||||
"paper-slider-active-color": "var(--accent-color)",
|
||||
"secondary-background-color": "#222222",
|
||||
"paper-slider-container-color":
|
||||
"linear-gradient(var(--primary-background-color), var(--secondary-background-color)) no-repeat",
|
||||
"paper-slider-disabled-active-color": "var(--disabled-text-color)",
|
||||
"disabled-text-color": "#777777",
|
||||
"paper-item-icon_-_color": "green",
|
||||
"paper-grey-200": "#222222",
|
||||
@ -33,14 +28,10 @@ export const demoThemeKernehed = () => ({
|
||||
"switch-unchecked-button-color": "var(--disabled-text-color)",
|
||||
"label-badge-border-color": "green",
|
||||
"paper-listbox-color": "#777777",
|
||||
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
|
||||
"card-background-color": "#292929",
|
||||
"label-badge-text-color": "var(--primary-text-color)",
|
||||
"paper-slider-knob-start-color": "var(--accent-color)",
|
||||
"switch-unchecked-track-color": "var(--disabled-text-color)",
|
||||
"dark-primary-color": "var(--accent-color)",
|
||||
"paper-slider-secondary-color": "var(--secondary-background-color)",
|
||||
"paper-slider-pin-color": "var(--accent-color)",
|
||||
"paper-item-icon-active-color": "#b58e31",
|
||||
"accent-color": "#2980b9",
|
||||
"table-row-alternative-background-color": "#292929",
|
||||
|
@ -3,6 +3,15 @@ import { DemoConfig } from "../types";
|
||||
|
||||
export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
convertEntities({
|
||||
"todo.shopping_list": {
|
||||
entity_id: "todo.shopping_list",
|
||||
state: "2",
|
||||
attributes: {
|
||||
supported_features: 15,
|
||||
friendly_name: "Shopping List",
|
||||
icon: "mdi:cart",
|
||||
},
|
||||
},
|
||||
"sensor.pollen_grabo": {
|
||||
entity_id: "sensor.pollen_grabo",
|
||||
state: "",
|
||||
|
@ -220,7 +220,8 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
state_filter: ["on"],
|
||||
},
|
||||
{
|
||||
type: "shopping-list",
|
||||
type: "todo-list",
|
||||
entity: "todo.shopping_list",
|
||||
},
|
||||
{
|
||||
entities: [
|
||||
|
@ -1,6 +1,5 @@
|
||||
export const demoThemeTeachingbirds = () => ({
|
||||
"paper-card-header-color": "var(--paper-item-icon-color)",
|
||||
"paper-slider-pin-color": "var(--primary-color)",
|
||||
"paper-listbox-background-color": "#202020",
|
||||
"paper-grey-50": "var(--primary-text-color)",
|
||||
"paper-item-icon-color": "#d3d3d3",
|
||||
@ -8,8 +7,6 @@ export const demoThemeTeachingbirds = () => ({
|
||||
"primary-color": "#389638",
|
||||
"light-primary-color": "#6f956f",
|
||||
"label-badge-red": "var(--primary-color)",
|
||||
"paper-slider-secondary-color": "var(--light-primary-color)",
|
||||
"paper-slider-knob-color": "var(--primary-color)",
|
||||
"paper-listbox-color": "#FFFFFF",
|
||||
"paper-toggle-button-checked-bar-color": "var(--light-primary-color)",
|
||||
"switch-unchecked-track-color": "var(--primary-text-color)",
|
||||
@ -17,9 +14,7 @@ export const demoThemeTeachingbirds = () => ({
|
||||
"label-badge-text-color": "var(--text-primary-color)",
|
||||
"primary-background-color": "#303030",
|
||||
"sidebar-icon-color": "var(--paper-item-icon-color)",
|
||||
"paper-slider-active-color": "#d8bf50",
|
||||
"secondary-background-color": "#2b2b2b",
|
||||
"paper-slider-knob-start-color": "var(--primary-color)",
|
||||
"paper-item-icon-active-color": "#d8bf50",
|
||||
"switch-checked-color": "var(--primary-color)",
|
||||
"secondary-text-color": "#389638",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import "../../src/resources/ha-style";
|
||||
import "../../src/resources/roboto";
|
||||
import "../../src/resources/safari-14-attachshadow-patch";
|
||||
import "./ha-demo";
|
||||
|
||||
import("../../src/resources/ha-style");
|
||||
|
@ -22,7 +22,7 @@ import { mockLovelace } from "./stubs/lovelace";
|
||||
import { mockMediaPlayer } from "./stubs/media_player";
|
||||
import { mockPersistentNotification } from "./stubs/persistent_notification";
|
||||
import { mockRecorder } from "./stubs/recorder";
|
||||
import { mockShoppingList } from "./stubs/shopping_list";
|
||||
import { mockTodo } from "./stubs/todo";
|
||||
import { mockSystemLog } from "./stubs/system_log";
|
||||
import { mockTemplate } from "./stubs/template";
|
||||
import { mockTranslations } from "./stubs/translations";
|
||||
@ -49,7 +49,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockTranslations(hass);
|
||||
mockHistory(hass);
|
||||
mockRecorder(hass);
|
||||
mockShoppingList(hass);
|
||||
mockTodo(hass);
|
||||
mockSystemLog(hass);
|
||||
mockTemplate(hass);
|
||||
mockEvents(hass);
|
||||
|
@ -1,44 +0,0 @@
|
||||
import { ShoppingListItem } from "../../../src/data/shopping-list";
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
let items: ShoppingListItem[] = [
|
||||
{
|
||||
id: 12,
|
||||
name: "Milk",
|
||||
complete: false,
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Eggs",
|
||||
complete: false,
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Oranges",
|
||||
complete: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockShoppingList = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("shopping_list/items", () => items);
|
||||
hass.mockWS("shopping_list/items/add", (msg) => {
|
||||
const item: ShoppingListItem = {
|
||||
id: new Date().getTime(),
|
||||
complete: false,
|
||||
name: msg.name,
|
||||
};
|
||||
items.push(item);
|
||||
hass.mockEvent("shopping_list_updated");
|
||||
return item;
|
||||
});
|
||||
hass.mockWS("shopping_list/items/update", ({ type, item_id, ...updates }) => {
|
||||
items = items.map((item) =>
|
||||
item.id === item_id ? { ...item, ...updates } : item
|
||||
);
|
||||
hass.mockEvent("shopping_list_updated");
|
||||
});
|
||||
hass.mockWS("shopping_list/items/clear", () => {
|
||||
items = items.filter((item) => !item.complete);
|
||||
hass.mockEvent("shopping_list_updated");
|
||||
});
|
||||
};
|
24
demo/src/stubs/todo.ts
Normal file
24
demo/src/stubs/todo.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { TodoItem, TodoItemStatus } from "../../../src/data/todo";
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockTodo = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("todo/item/list", () => ({
|
||||
items: [
|
||||
{
|
||||
uid: "12",
|
||||
summary: "Milk",
|
||||
status: TodoItemStatus.NeedsAction,
|
||||
},
|
||||
{
|
||||
uid: "13",
|
||||
summary: "Eggs",
|
||||
status: TodoItemStatus.NeedsAction,
|
||||
},
|
||||
{
|
||||
uid: "14",
|
||||
summary: "Oranges",
|
||||
status: TodoItemStatus.Completed,
|
||||
},
|
||||
] as TodoItem[],
|
||||
}));
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import "../../src/resources/ha-style";
|
||||
import "../../src/resources/roboto";
|
||||
import "./ha-gallery";
|
||||
|
||||
import("../../src/resources/ha-style");
|
||||
|
||||
document.body.appendChild(document.createElement("ha-gallery"));
|
||||
|
@ -49,11 +49,11 @@ export class DemoHaCircularSlider extends LitElement {
|
||||
<div class="field">
|
||||
<p>Current</p>
|
||||
<ha-slider
|
||||
labeled
|
||||
min="10"
|
||||
max="30"
|
||||
.value=${this.current}
|
||||
@change=${this._currentChanged}
|
||||
pin
|
||||
></ha-slider>
|
||||
<p>${this.current} °C</p>
|
||||
</div>
|
||||
|
@ -57,6 +57,7 @@ const DEVICES = [
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
@ -74,6 +75,7 @@ const DEVICES = [
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
@ -91,6 +93,7 @@ const DEVICES = [
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -57,8 +57,8 @@ export class DemoHaHsColorPicker extends LitElement {
|
||||
></ha-hs-color-picker>
|
||||
<p>Hue : ${this.value[0]}</p>
|
||||
<ha-slider
|
||||
labeled
|
||||
step="1"
|
||||
pin
|
||||
min="0"
|
||||
max="360"
|
||||
.value=${this.value[0]}
|
||||
@ -67,8 +67,8 @@ export class DemoHaHsColorPicker extends LitElement {
|
||||
</ha-slider>
|
||||
<p>Saturation : ${this.value[1]}</p>
|
||||
<ha-slider
|
||||
labeled
|
||||
step="0.01"
|
||||
pin
|
||||
min="0"
|
||||
max="1"
|
||||
.value=${this.value[1]}
|
||||
@ -77,8 +77,8 @@ export class DemoHaHsColorPicker extends LitElement {
|
||||
</ha-slider>
|
||||
<p>Color Brighness : ${this.brightness}</p>
|
||||
<ha-slider
|
||||
labeled
|
||||
step="1"
|
||||
pin
|
||||
min="0"
|
||||
max="255"
|
||||
.value=${this.brightness}
|
||||
|
@ -53,6 +53,7 @@ const DEVICES = [
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
@ -70,6 +71,7 @@ const DEVICES = [
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
@ -87,6 +89,7 @@ const DEVICES = [
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Shopping List Card
|
||||
---
|
3
gallery/src/pages/lovelace/todo-list-card.markdown
Normal file
3
gallery/src/pages/lovelace/todo-list-card.markdown
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Todo List Card
|
||||
---
|
@ -2,25 +2,39 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, query } from "lit/decorators";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-cards";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { mockTodo } from "../../../../demo/src/stubs/todo";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("todo", "shopping_list", "2", {
|
||||
friendly_name: "Shopping List",
|
||||
supported_features: 15,
|
||||
}),
|
||||
getEntity("todo", "read_only", "2", {
|
||||
friendly_name: "Read only",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
heading: "List example",
|
||||
config: `
|
||||
- type: shopping-list
|
||||
- type: todo-list
|
||||
entity: todo.shopping_list
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "List with title example",
|
||||
config: `
|
||||
- type: shopping-list
|
||||
- type: todo-list
|
||||
title: Shopping List
|
||||
entity: todo.read_only
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-lovelace-shopping-list-card")
|
||||
class DemoShoppingListEntity extends LitElement {
|
||||
@customElement("demo-lovelace-todo-list-card")
|
||||
class DemoTodoListEntity extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@ -32,18 +46,14 @@ class DemoShoppingListEntity extends LitElement {
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
|
||||
hass.mockAPI("shopping_list", () => [
|
||||
{ name: "list", id: 1, complete: false },
|
||||
{ name: "all", id: 2, complete: false },
|
||||
{ name: "the", id: 3, complete: false },
|
||||
{ name: "things", id: 4, complete: true },
|
||||
]);
|
||||
mockTodo(hass);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-lovelace-shopping-list-card": DemoShoppingListEntity;
|
||||
"demo-lovelace-todo-list-card": DemoTodoListEntity;
|
||||
}
|
||||
}
|
@ -213,6 +213,7 @@ const createDeviceRegistryEntries = (
|
||||
name: "Tag Reader",
|
||||
sw_version: null,
|
||||
hw_version: "1.0.0",
|
||||
serial_number: "00_12_4B_00_22_98_88_7F",
|
||||
id: "mock-device-id",
|
||||
identifiers: [],
|
||||
via_device_id: null,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { dump } from "js-yaml";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@ -9,7 +10,6 @@ import "../../../../src/components/ha-expansion-panel";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/search-input";
|
||||
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
|
||||
import { dump } from "../../../../src/resources/js-yaml-dump";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
|
||||
|
@ -1,15 +1,15 @@
|
||||
// Compat needs to be first import
|
||||
import "../../src/resources/compatibility";
|
||||
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
|
||||
import "../../src/resources/roboto";
|
||||
import "../../src/resources/ha-style";
|
||||
import "../../src/resources/safari-14-attachshadow-patch";
|
||||
import "./hassio-main";
|
||||
|
||||
setCancelSyntheticClickEvents(false);
|
||||
import("../../src/resources/ha-style");
|
||||
import("@polymer/polymer/lib/utils/settings").then(
|
||||
({ setCancelSyntheticClickEvents }) => setCancelSyntheticClickEvents(false)
|
||||
);
|
||||
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.innerHTML = `
|
||||
styleEl.textContent = `
|
||||
body {
|
||||
font-family: Roboto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
117
package.json
117
package.json
@ -25,24 +25,24 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.23.1",
|
||||
"@babel/runtime": "7.23.2",
|
||||
"@braintree/sanitize-url": "6.0.4",
|
||||
"@codemirror/autocomplete": "6.9.1",
|
||||
"@codemirror/autocomplete": "6.10.2",
|
||||
"@codemirror/commands": "6.3.0",
|
||||
"@codemirror/language": "6.9.1",
|
||||
"@codemirror/legacy-modes": "6.3.3",
|
||||
"@codemirror/search": "6.5.4",
|
||||
"@codemirror/state": "6.2.1",
|
||||
"@codemirror/view": "6.21.1",
|
||||
"@codemirror/state": "6.3.1",
|
||||
"@codemirror/view": "6.21.3",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.10.3",
|
||||
"@formatjs/intl-displaynames": "6.5.2",
|
||||
"@formatjs/intl-getcanonicallocales": "2.2.1",
|
||||
"@formatjs/intl-listformat": "7.4.2",
|
||||
"@formatjs/intl-locale": "3.3.4",
|
||||
"@formatjs/intl-numberformat": "8.7.2",
|
||||
"@formatjs/intl-pluralrules": "5.2.6",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.6",
|
||||
"@formatjs/intl-datetimeformat": "6.11.0",
|
||||
"@formatjs/intl-displaynames": "6.6.0",
|
||||
"@formatjs/intl-getcanonicallocales": "2.3.0",
|
||||
"@formatjs/intl-listformat": "7.5.0",
|
||||
"@formatjs/intl-locale": "3.4.0",
|
||||
"@formatjs/intl-numberformat": "8.8.0",
|
||||
"@formatjs/intl-pluralrules": "5.2.7",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.7",
|
||||
"@fullcalendar/core": "6.1.9",
|
||||
"@fullcalendar/daygrid": "6.1.9",
|
||||
"@fullcalendar/interaction": "6.1.9",
|
||||
@ -52,10 +52,12 @@
|
||||
"@lezer/highlight": "1.1.6",
|
||||
"@lit-labs/context": "0.4.1",
|
||||
"@lit-labs/motion": "1.0.4",
|
||||
"@lit-labs/observers": "2.0.1",
|
||||
"@lit-labs/virtualizer": "2.0.7",
|
||||
"@lrnwebcomponents/simple-tooltip": "7.0.18",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-base": "0.27.0",
|
||||
"@material/mwc-button": "0.27.0",
|
||||
"@material/mwc-checkbox": "0.27.0",
|
||||
"@material/mwc-circular-progress": "0.27.0",
|
||||
@ -71,7 +73,6 @@
|
||||
"@material/mwc-radio": "0.27.0",
|
||||
"@material/mwc-ripple": "0.27.0",
|
||||
"@material/mwc-select": "0.27.0",
|
||||
"@material/mwc-slider": "0.27.0",
|
||||
"@material/mwc-switch": "0.27.0",
|
||||
"@material/mwc-tab": "0.27.0",
|
||||
"@material/mwc-tab-bar": "0.27.0",
|
||||
@ -80,22 +81,21 @@
|
||||
"@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",
|
||||
"@mdi/js": "7.2.96",
|
||||
"@mdi/svg": "7.2.96",
|
||||
"@material/web": "=1.0.1",
|
||||
"@mdi/js": "7.3.67",
|
||||
"@mdi/svg": "7.3.67",
|
||||
"@polymer/iron-flex-layout": "3.0.1",
|
||||
"@polymer/iron-input": "3.0.1",
|
||||
"@polymer/iron-resizable-behavior": "3.0.1",
|
||||
"@polymer/paper-input": "3.2.1",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
"@polymer/paper-listbox": "3.0.1",
|
||||
"@polymer/paper-slider": "3.0.1",
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/paper-toast": "3.0.1",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.1.10",
|
||||
"@vaadin/vaadin-themable-mixin": "24.1.10",
|
||||
"@vaadin/combo-box": "24.2.0",
|
||||
"@vaadin/vaadin-themable-mixin": "24.2.0",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@ -105,7 +105,7 @@
|
||||
"app-datepicker": "5.1.1",
|
||||
"chart.js": "4.4.0",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.32.2",
|
||||
"core-js": "3.33.1",
|
||||
"cropperjs": "1.6.1",
|
||||
"date-fns": "2.30.0",
|
||||
"date-fns-tz": "2.0.0",
|
||||
@ -114,15 +114,15 @@
|
||||
"fuse.js": "6.6.2",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"hls.js": "1.4.12",
|
||||
"home-assistant-js-websocket": "8.2.0",
|
||||
"home-assistant-js-websocket": "9.1.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.5.3",
|
||||
"intl-messageformat": "10.5.4",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "1.0.4",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.4.3",
|
||||
"marked": "9.0.3",
|
||||
"marked": "9.1.2",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
@ -141,7 +141,7 @@
|
||||
"ua-parser-js": "1.0.36",
|
||||
"unfetch": "5.0.0",
|
||||
"vis-data": "7.1.7",
|
||||
"vis-network": "9.1.6",
|
||||
"vis-network": "9.1.8",
|
||||
"vue": "2.7.14",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
@ -154,48 +154,49 @@
|
||||
"xss": "1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.23.0",
|
||||
"@babel/plugin-proposal-decorators": "7.23.0",
|
||||
"@babel/plugin-transform-runtime": "7.22.15",
|
||||
"@babel/preset-env": "7.22.20",
|
||||
"@babel/preset-typescript": "7.23.0",
|
||||
"@babel/core": "7.23.2",
|
||||
"@babel/plugin-proposal-decorators": "7.23.2",
|
||||
"@babel/plugin-transform-runtime": "7.23.2",
|
||||
"@babel/preset-env": "7.23.2",
|
||||
"@babel/preset-typescript": "7.23.2",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.7.7",
|
||||
"@koa/cors": "4.0.0",
|
||||
"@lokalise/node-api": "12.0.0",
|
||||
"@octokit/auth-oauth-device": "6.0.1",
|
||||
"@octokit/plugin-retry": "6.0.1",
|
||||
"@octokit/rest": "20.0.2",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rollup/plugin-babel": "6.0.3",
|
||||
"@rollup/plugin-commonjs": "25.0.4",
|
||||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/plugin-node-resolve": "15.2.1",
|
||||
"@rollup/plugin-replace": "5.0.2",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.3",
|
||||
"@types/chromecast-caf-receiver": "6.0.10",
|
||||
"@types/chromecast-caf-sender": "1.0.6",
|
||||
"@types/esprima": "4.0.4",
|
||||
"@rollup/plugin-babel": "6.0.4",
|
||||
"@rollup/plugin-commonjs": "25.0.7",
|
||||
"@rollup/plugin-json": "6.0.1",
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-replace": "5.0.4",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.4",
|
||||
"@types/chromecast-caf-receiver": "6.0.11",
|
||||
"@types/chromecast-caf-sender": "1.0.7",
|
||||
"@types/esprima": "4.0.5",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.0",
|
||||
"@types/js-yaml": "4.0.6",
|
||||
"@types/leaflet": "1.9.6",
|
||||
"@types/leaflet-draw": "1.0.8",
|
||||
"@types/luxon": "3.3.2",
|
||||
"@types/mocha": "10.0.2",
|
||||
"@types/qrcode": "1.5.2",
|
||||
"@types/serve-handler": "6.1.2",
|
||||
"@types/sortablejs": "1.15.3",
|
||||
"@types/tar": "6.1.6",
|
||||
"@types/ua-parser-js": "0.7.37",
|
||||
"@types/html-minifier-terser": "7.0.1",
|
||||
"@types/js-yaml": "4.0.8",
|
||||
"@types/leaflet": "1.9.7",
|
||||
"@types/leaflet-draw": "1.0.9",
|
||||
"@types/luxon": "3.3.3",
|
||||
"@types/mocha": "10.0.3",
|
||||
"@types/qrcode": "1.5.4",
|
||||
"@types/serve-handler": "6.1.3",
|
||||
"@types/sortablejs": "1.15.4",
|
||||
"@types/tar": "6.1.7",
|
||||
"@types/ua-parser-js": "0.7.38",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.3",
|
||||
"@typescript-eslint/parser": "6.7.3",
|
||||
"@typescript-eslint/eslint-plugin": "6.8.0",
|
||||
"@typescript-eslint/parser": "6.8.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.10",
|
||||
"del": "7.1.0",
|
||||
"eslint": "8.50.0",
|
||||
"eslint": "8.52.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "17.1.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
@ -220,10 +221,10 @@
|
||||
"husky": "8.0.3",
|
||||
"instant-mocha": "1.5.2",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "14.0.1",
|
||||
"lint-staged": "15.0.2",
|
||||
"lit-analyzer": "2.0.0-pre.3",
|
||||
"lodash.template": "4.5.0",
|
||||
"magic-string": "0.30.4",
|
||||
"magic-string": "0.30.5",
|
||||
"map-stream": "0.0.7",
|
||||
"mocha": "10.2.0",
|
||||
"object-hash": "3.0.0",
|
||||
@ -235,7 +236,7 @@
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.9.2",
|
||||
"serve-handler": "6.1.5",
|
||||
"sinon": "16.0.0",
|
||||
"sinon": "17.0.0",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.14.2",
|
||||
"tar": "6.2.0",
|
||||
@ -244,10 +245,11 @@
|
||||
"typescript": "5.2.2",
|
||||
"vinyl-buffer": "1.0.1",
|
||||
"vinyl-source-stream": "2.0.0",
|
||||
"webpack": "5.88.2",
|
||||
"webpack": "5.89.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "4.15.1",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "5.0.2",
|
||||
"workbox-build": "7.0.0"
|
||||
},
|
||||
@ -255,8 +257,9 @@
|
||||
"resolutions": {
|
||||
"@polymer/polymer": "patch:@polymer/polymer@3.5.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
|
||||
"@material/mwc-button@^0.25.3": "^0.27.0",
|
||||
"lit@^2.7.4 || ^3.0.0": "^2.7.4",
|
||||
"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.3"
|
||||
"packageManager": "yarn@3.6.4"
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20231005.0"
|
||||
version = "20231025.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@ -33,7 +33,7 @@ fi
|
||||
|
||||
docker run \
|
||||
-v ${LOCAL_FILE}:/opt/src/${LOCAL_FILE} \
|
||||
lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d lokalise2 \
|
||||
lokalise/lokalise-cli-2:v2.6.10 lokalise2 \
|
||||
--token ${LOKALISE_TOKEN} \
|
||||
--project-id ${PROJECT_ID} \
|
||||
file upload \
|
||||
|
@ -26,14 +26,13 @@ export class HaAuthFormString extends HaFormString {
|
||||
}
|
||||
ha-auth-form-string ha-icon-button {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 12px;
|
||||
--mdc-icon-button-size: 24px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-auth-form-string ha-icon-button {
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 12px;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
</style>
|
||||
@ -63,7 +62,7 @@ export class HaAuthFormString extends HaFormString {
|
||||
.validationMessage=${this.schema.required ? "Required" : undefined}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
></ha-auth-textfield>
|
||||
></ha-auth-textfield>
|
||||
${this.renderIcon()}
|
||||
</ha-auth-textfield>
|
||||
`;
|
||||
|
9
src/common/array/combinations.ts
Normal file
9
src/common/array/combinations.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export function getAllCombinations<T>(arr: T[]) {
|
||||
return arr.reduce<T[][]>(
|
||||
(combinations, element) =>
|
||||
combinations.concat(
|
||||
combinations.map((combination) => [...combination, element])
|
||||
),
|
||||
[[]]
|
||||
);
|
||||
}
|
@ -16,6 +16,7 @@ import {
|
||||
mdiCarCoolantLevel,
|
||||
mdiCash,
|
||||
mdiChatSleep,
|
||||
mdiClipboardList,
|
||||
mdiClock,
|
||||
mdiCloudUpload,
|
||||
mdiCog,
|
||||
@ -120,6 +121,7 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
siren: mdiBullhorn,
|
||||
stt: mdiMicrophoneMessage,
|
||||
text: mdiFormTextbox,
|
||||
todo: mdiClipboardList,
|
||||
time: mdiClock,
|
||||
timer: mdiTimerOutline,
|
||||
tts: mdiSpeakerMessage,
|
||||
|
@ -5,12 +5,15 @@ import { FrontendLocaleData, TimeZone } from "../../data/translation";
|
||||
const calcZonedDate = (
|
||||
date: Date,
|
||||
tz: string,
|
||||
fn: (date: Date, options?: any) => Date,
|
||||
fn: (date: Date, options?: any) => Date | number | boolean,
|
||||
options?
|
||||
) => {
|
||||
const inputZoned = utcToZonedTime(date, tz);
|
||||
const fnZoned = fn(inputZoned, options);
|
||||
return zonedTimeToUtc(fnZoned, tz);
|
||||
if (fnZoned instanceof Date) {
|
||||
return zonedTimeToUtc(fnZoned, tz) as Date;
|
||||
}
|
||||
return fnZoned;
|
||||
};
|
||||
|
||||
export const calcDate = (
|
||||
@ -21,5 +24,16 @@ export const calcDate = (
|
||||
options?
|
||||
) =>
|
||||
locale.time_zone === TimeZone.server
|
||||
? calcZonedDate(date, config.time_zone, fn, options)
|
||||
? (calcZonedDate(date, config.time_zone, fn, options) as Date)
|
||||
: fn(date, options);
|
||||
|
||||
export const calcDateProperty = (
|
||||
date: Date,
|
||||
fn: (date: Date, options?: any) => boolean | number,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
options?
|
||||
) =>
|
||||
locale.time_zone === TimeZone.server
|
||||
? (calcZonedDate(date, config.time_zone, fn, options) as number | boolean)
|
||||
: fn(date, options);
|
||||
|
@ -37,6 +37,23 @@ const formatDateMem = memoizeOne(
|
||||
})
|
||||
);
|
||||
|
||||
// Aug 10, 2021
|
||||
export const formatDateShort = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateShortMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateShortMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// 10/08/2021
|
||||
export const formatDateNumeric = (
|
||||
dateObj: Date,
|
||||
@ -102,13 +119,13 @@ const formatDateNumericMem = memoizeOne(
|
||||
);
|
||||
|
||||
// Aug 10
|
||||
export const formatDateShort = (
|
||||
export const formatDateVeryShort = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateShortMem(locale, config.time_zone).format(dateObj);
|
||||
) => formatDateVeryShortMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateShortMem = memoizeOne(
|
||||
const formatDateVeryShortMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
day: "numeric",
|
||||
|
@ -1,8 +1,13 @@
|
||||
import { HaDurationData } from "../../components/ha-duration-input";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import "../../resources/intl-polyfill";
|
||||
|
||||
const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
|
||||
|
||||
export const formatDuration = (duration: HaDurationData) => {
|
||||
export const formatDuration = (
|
||||
locale: FrontendLocaleData,
|
||||
duration: HaDurationData
|
||||
) => {
|
||||
const d = duration.days || 0;
|
||||
const h = duration.hours || 0;
|
||||
const m = duration.minutes || 0;
|
||||
@ -10,7 +15,11 @@ export const formatDuration = (duration: HaDurationData) => {
|
||||
const ms = duration.milliseconds || 0;
|
||||
|
||||
if (d > 0) {
|
||||
return `${d} day${d === 1 ? "" : "s"} ${h}:${leftPad(m)}:${leftPad(s)}`;
|
||||
return `${Intl.NumberFormat(locale.language, {
|
||||
style: "unit",
|
||||
unit: "day",
|
||||
unitDisplay: "long",
|
||||
}).format(d)} ${h}:${leftPad(m)}:${leftPad(s)}`;
|
||||
}
|
||||
if (h > 0) {
|
||||
return `${h}:${leftPad(m)}:${leftPad(s)}`;
|
||||
@ -19,10 +28,18 @@ export const formatDuration = (duration: HaDurationData) => {
|
||||
return `${m}:${leftPad(s)}`;
|
||||
}
|
||||
if (s > 0) {
|
||||
return `${s} second${s === 1 ? "" : "s"}`;
|
||||
return Intl.NumberFormat(locale.language, {
|
||||
style: "unit",
|
||||
unit: "second",
|
||||
unitDisplay: "long",
|
||||
}).format(s);
|
||||
}
|
||||
if (ms > 0) {
|
||||
return `${ms} millisecond${ms === 1 ? "" : "s"}`;
|
||||
return Intl.NumberFormat(locale.language, {
|
||||
style: "unit",
|
||||
unit: "millisecond",
|
||||
unitDisplay: "long",
|
||||
}).format(ms);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
@ -41,9 +41,7 @@ export const applyThemesOnElement = (
|
||||
// If there is no explicitly desired dark mode provided, we automatically
|
||||
// use the active one from `themes`.
|
||||
const darkMode =
|
||||
themeSettings && themeSettings?.dark !== undefined
|
||||
? themeSettings?.dark
|
||||
: themes.darkMode;
|
||||
themeSettings?.dark !== undefined ? themeSettings.dark : themes.darkMode;
|
||||
|
||||
let cacheKey = themeToApply;
|
||||
let themeRules: Partial<ThemeVars> = {};
|
||||
@ -135,10 +133,19 @@ export const applyThemesOnElement = (
|
||||
|
||||
// Set and/or reset styles
|
||||
if (element.updateStyles) {
|
||||
// Use updateStyles() method of Polymer elements
|
||||
element.updateStyles(styles);
|
||||
} else if (window.ShadyCSS) {
|
||||
// Implement updateStyles() method of Polymer elements
|
||||
// Use ShadyCSS if available
|
||||
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
|
||||
} else {
|
||||
for (const s in styles) {
|
||||
if (s === null) {
|
||||
element.style.removeProperty(s);
|
||||
} else {
|
||||
element.style.setProperty(s, styles[s]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,19 +1,29 @@
|
||||
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
|
||||
export const slugify = (value: string, delimiter = "_") => {
|
||||
const a =
|
||||
"àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;";
|
||||
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`;
|
||||
"àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·";
|
||||
const b = `aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}`;
|
||||
const p = new RegExp(a.split("").join("|"), "g");
|
||||
|
||||
return value
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, delimiter) // Replace spaces with delimiter
|
||||
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
|
||||
.replace(/&/g, `${delimiter}and${delimiter}`) // Replace & with 'and'
|
||||
.replace(/[^\w-]+/g, "") // Remove all non-word characters
|
||||
.replace(/-/g, delimiter) // Replace - with delimiter
|
||||
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
|
||||
.replace(new RegExp(`^${delimiter}+`), "") // Trim delimiter from start of text
|
||||
.replace(new RegExp(`${delimiter}+$`), ""); // Trim delimiter from end of text
|
||||
let slugified;
|
||||
|
||||
if (value === "") {
|
||||
slugified = "";
|
||||
} else {
|
||||
slugified = value
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
|
||||
.replace(/(?<=\d),(?=\d)/g, "") // Remove Commas between numbers
|
||||
.replace(/[^a-z0-9]+/g, delimiter) // Replace all non-word characters
|
||||
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
|
||||
.replace(new RegExp(`^${delimiter}+`), "") // Trim delimiter from start of text
|
||||
.replace(new RegExp(`${delimiter}+$`), ""); // Trim delimiter from end of text
|
||||
|
||||
if (slugified === "") {
|
||||
slugified = "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
return slugified;
|
||||
};
|
||||
|
@ -13,45 +13,33 @@ export const handleStructError = (
|
||||
for (const failure of err.failures()) {
|
||||
if (failure.value === undefined) {
|
||||
errors.push(
|
||||
hass.localize(
|
||||
"ui.errors.config.key_missing",
|
||||
"key",
|
||||
failure.path.join(".")
|
||||
)
|
||||
hass.localize("ui.errors.config.key_missing", {
|
||||
key: failure.path.join("."),
|
||||
})
|
||||
);
|
||||
} else if (failure.type === "never") {
|
||||
warnings.push(
|
||||
hass.localize(
|
||||
"ui.errors.config.key_not_expected",
|
||||
"key",
|
||||
failure.path.join(".")
|
||||
)
|
||||
hass.localize("ui.errors.config.key_not_expected", {
|
||||
key: failure.path.join("."),
|
||||
})
|
||||
);
|
||||
} else if (failure.type === "union") {
|
||||
continue;
|
||||
} else if (failure.type === "enums") {
|
||||
warnings.push(
|
||||
hass.localize(
|
||||
"ui.errors.config.key_wrong_type",
|
||||
"key",
|
||||
failure.path.join("."),
|
||||
"type_correct",
|
||||
failure.message.replace("Expected ", "").split(", ")[0],
|
||||
"type_wrong",
|
||||
JSON.stringify(failure.value)
|
||||
)
|
||||
hass.localize("ui.errors.config.key_wrong_type", {
|
||||
key: failure.path.join("."),
|
||||
type_correct: failure.message.replace("Expected ", "").split(", ")[0],
|
||||
type_wrong: JSON.stringify(failure.value),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
warnings.push(
|
||||
hass.localize(
|
||||
"ui.errors.config.key_wrong_type",
|
||||
"key",
|
||||
failure.path.join("."),
|
||||
"type_correct",
|
||||
failure.refinement || failure.type,
|
||||
"type_wrong",
|
||||
JSON.stringify(failure.value)
|
||||
)
|
||||
hass.localize("ui.errors.config.key_wrong_type", {
|
||||
key: failure.path.join("."),
|
||||
type_correct: failure.refinement || failure.type,
|
||||
type_wrong: JSON.stringify(failure.value),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,13 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../ha-circular-progress";
|
||||
import "../ha-svg-icon";
|
||||
@ -27,7 +34,7 @@ export class HaProgressButton extends LitElement {
|
||||
<slot></slot>
|
||||
</mwc-button>
|
||||
${!overlay
|
||||
? ""
|
||||
? nothing
|
||||
: html`
|
||||
<div class="progress">
|
||||
${this._result === "success"
|
||||
|
@ -39,7 +39,7 @@ import {
|
||||
formatDate,
|
||||
formatDateMonth,
|
||||
formatDateMonthYear,
|
||||
formatDateShort,
|
||||
formatDateVeryShort,
|
||||
formatDateWeekdayDay,
|
||||
formatDateYear,
|
||||
} from "../../common/datetime/format_date";
|
||||
@ -128,7 +128,7 @@ _adapters._date.override({
|
||||
this.options.config
|
||||
);
|
||||
case "day":
|
||||
return formatDateShort(
|
||||
return formatDateVeryShort(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
|
@ -12,6 +12,7 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import { clamp } from "../../common/number/clamp";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
|
||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
|
||||
@ -52,6 +53,12 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
@state() private _hiddenDatasets: Set<number> = new Set();
|
||||
|
||||
private _paddingUpdateCount = 0;
|
||||
|
||||
private _paddingUpdateLock = false;
|
||||
|
||||
private _paddingYAxisInternal = 0;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._releaseCanvas();
|
||||
@ -104,9 +111,44 @@ export class HaChartBase extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
public shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (
|
||||
this._paddingUpdateLock &&
|
||||
changedProps.size === 1 &&
|
||||
changedProps.has("paddingYAxis")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _debouncedClearUpdates = debounce(
|
||||
() => {
|
||||
this._paddingUpdateCount = 0;
|
||||
},
|
||||
2000,
|
||||
false
|
||||
);
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this._paddingUpdateLock) {
|
||||
this._paddingYAxisInternal = this.paddingYAxis;
|
||||
if (changedProps.size === 1 && changedProps.has("paddingYAxis")) {
|
||||
this._paddingUpdateCount++;
|
||||
if (this._paddingUpdateCount > 300) {
|
||||
this._paddingUpdateLock = true;
|
||||
// eslint-disable-next-line
|
||||
console.error(
|
||||
"Detected excessive chart padding updates, possibly an infinite loop. Disabling axis padding."
|
||||
);
|
||||
} else {
|
||||
this._debouncedClearUpdates();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.hasUpdated || !this.chart) {
|
||||
return;
|
||||
}
|
||||
@ -171,10 +213,10 @@ export class HaChartBase extends LitElement {
|
||||
this.height ?? this._chartHeight ?? this.clientWidth / 2
|
||||
}px`,
|
||||
"padding-left": `${
|
||||
computeRTL(this.hass) ? 0 : this.paddingYAxis
|
||||
computeRTL(this.hass) ? 0 : this._paddingYAxisInternal
|
||||
}px`,
|
||||
"padding-right": `${
|
||||
computeRTL(this.hass) ? this.paddingYAxis : 0
|
||||
computeRTL(this.hass) ? this._paddingYAxisInternal : 0
|
||||
}px`,
|
||||
})}
|
||||
>
|
||||
@ -324,7 +366,7 @@ export class HaChartBase extends LitElement {
|
||||
clamp(
|
||||
context.tooltip.caretX,
|
||||
100,
|
||||
this.clientWidth - 100 - this.paddingYAxis
|
||||
this.clientWidth - 100 - this._paddingYAxisInternal
|
||||
) -
|
||||
100 +
|
||||
"px",
|
||||
|
@ -141,16 +141,10 @@ export class StateHistoryChartLine extends LitElement {
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
context.parsed.y,
|
||||
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
|
||||
getNumberFormatOptions(
|
||||
undefined,
|
||||
this.hass.entities[this._entityIds[context.datasetIndex]]
|
||||
)
|
||||
)} ${this.unit}`,
|
||||
},
|
||||
},
|
||||
|
@ -73,9 +73,9 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public isLoadingData = false;
|
||||
|
||||
@state() private _computedStartTime!: Date;
|
||||
private _computedStartTime!: Date;
|
||||
|
||||
@state() private _computedEndTime!: Date;
|
||||
private _computedEndTime!: Date;
|
||||
|
||||
@state() private _maxYWidth = 0;
|
||||
|
||||
@ -114,31 +114,6 @@ export class StateHistoryCharts extends LitElement {
|
||||
${this.hass.localize("ui.components.history_charts.no_history_found")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
this._computedEndTime =
|
||||
this.upToNow || !this.endTime || this.endTime > now ? now : this.endTime;
|
||||
|
||||
if (this.startTime) {
|
||||
this._computedStartTime = this.startTime;
|
||||
} else if (this.hoursToShow) {
|
||||
this._computedStartTime = new Date(
|
||||
new Date().getTime() - 60 * 60 * this.hoursToShow * 1000
|
||||
);
|
||||
} else {
|
||||
this._computedStartTime = new Date(
|
||||
this.historyData.timeline.reduce(
|
||||
(minTime, stateInfo) =>
|
||||
Math.min(
|
||||
minTime,
|
||||
new Date(stateInfo.data[0].last_changed).getTime()
|
||||
),
|
||||
new Date().getTime()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const combinedItems = this.historyData.timeline.length
|
||||
? (this.virtualize
|
||||
? chunkData(this.historyData.timeline, CANVAS_TIMELINE_ROWS_CHUNK)
|
||||
@ -220,10 +195,45 @@ export class StateHistoryCharts extends LitElement {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected willUpdate() {
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
}
|
||||
if (
|
||||
[...changedProps.keys()].some(
|
||||
(prop) =>
|
||||
!(
|
||||
["_maxYWidth", "_childYWidths", "_chartCount"] as PropertyKey[]
|
||||
).includes(prop)
|
||||
)
|
||||
) {
|
||||
// Don't recompute times when we just want to update layout
|
||||
const now = new Date();
|
||||
|
||||
this._computedEndTime =
|
||||
this.upToNow || !this.endTime || this.endTime > now
|
||||
? now
|
||||
: this.endTime;
|
||||
|
||||
if (this.startTime) {
|
||||
this._computedStartTime = this.startTime;
|
||||
} else if (this.hoursToShow) {
|
||||
this._computedStartTime = new Date(
|
||||
new Date().getTime() - 60 * 60 * this.hoursToShow * 1000
|
||||
);
|
||||
} else {
|
||||
this._computedStartTime = new Date(
|
||||
this.historyData.timeline.reduce(
|
||||
(minTime, stateInfo) =>
|
||||
Math.min(
|
||||
minTime,
|
||||
new Date(stateInfo.data[0].last_changed).getTime()
|
||||
),
|
||||
new Date().getTime()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
|
@ -19,6 +19,7 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
getNumberFormatOptions,
|
||||
} from "../../common/number/format_number";
|
||||
import {
|
||||
getDisplayUnit,
|
||||
@ -74,6 +75,8 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@state() private _chartData: ChartData = { datasets: [] };
|
||||
|
||||
@state() private _statisticIds: string[] = [];
|
||||
|
||||
@state() private _chartOptions?: ChartOptions;
|
||||
|
||||
@query("ha-chart-base") private _chart?: HaChartBase;
|
||||
@ -189,7 +192,11 @@ export class StatisticsChart extends LitElement {
|
||||
label: (context) =>
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
context.parsed.y,
|
||||
this.hass.locale
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(
|
||||
undefined,
|
||||
this.hass.entities[this._statisticIds[context.datasetIndex]]
|
||||
)
|
||||
)} ${
|
||||
// @ts-ignore
|
||||
context.dataset.unit || ""
|
||||
@ -248,6 +255,7 @@ export class StatisticsChart extends LitElement {
|
||||
let colorIndex = 0;
|
||||
const statisticsData = Object.entries(this.statisticsData);
|
||||
const totalDataSets: ChartDataset<"line">[] = [];
|
||||
const statisticIds: string[] = [];
|
||||
let endTime: Date;
|
||||
|
||||
if (statisticsData.length === 0) {
|
||||
@ -386,6 +394,7 @@ export class StatisticsChart extends LitElement {
|
||||
unit: meta?.unit_of_measurement,
|
||||
band,
|
||||
});
|
||||
statisticIds.push(statistic_id);
|
||||
}
|
||||
});
|
||||
|
||||
@ -411,11 +420,7 @@ export class StatisticsChart extends LitElement {
|
||||
} else {
|
||||
val = stat[type];
|
||||
}
|
||||
dataValues.push(
|
||||
val !== null && val !== undefined
|
||||
? Math.round(val * 100) / 100
|
||||
: null
|
||||
);
|
||||
dataValues.push(val ?? null);
|
||||
});
|
||||
pushData(startDate, new Date(stat.end), dataValues);
|
||||
});
|
||||
@ -431,6 +436,7 @@ export class StatisticsChart extends LitElement {
|
||||
this._chartData = {
|
||||
datasets: totalDataSets,
|
||||
};
|
||||
this._statisticIds = statisticIds;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@ -31,6 +31,10 @@ const Component = Vue.extend({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
openingDirection: {
|
||||
type: String,
|
||||
default: "right",
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@ -66,7 +70,7 @@ const Component = Vue.extend({
|
||||
props: {
|
||||
"time-picker": this.timePicker,
|
||||
"auto-apply": this.autoApply,
|
||||
opens: "right",
|
||||
opens: this.openingDirection,
|
||||
"show-dropdowns": false,
|
||||
"time-picker24-hour": this.twentyfourHours,
|
||||
disabled: this.disabled,
|
||||
@ -126,9 +130,9 @@ class DateRangePickerElement extends WrappedElement {
|
||||
${dateRangePickerStyles}
|
||||
.calendars {
|
||||
display: flex;
|
||||
flex-wrap: nowrap !important;
|
||||
}
|
||||
.daterangepicker {
|
||||
left: 0px !important;
|
||||
top: auto;
|
||||
box-shadow: var(--ha-card-box-shadow, none);
|
||||
background-color: var(--card-background-color);
|
||||
@ -252,6 +256,10 @@ class DateRangePickerElement extends WrappedElement {
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
}
|
||||
.vue-daterange-picker{
|
||||
min-width: unset !important;
|
||||
display: block !important;
|
||||
}
|
||||
`;
|
||||
const shadowRoot = this.shadowRoot!;
|
||||
shadowRoot.appendChild(style);
|
||||
|
@ -87,6 +87,8 @@ export class HaStatisticPicker extends LitElement {
|
||||
@property({ type: Array, attribute: "exclude-statistics" })
|
||||
public excludeStatistics?: string[];
|
||||
|
||||
@property() public helpMissingEntityUrl = "/more-info/statistics/";
|
||||
|
||||
@state() private _opened?: boolean;
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
@ -111,7 +113,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
? html`<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${documentationUrl(this.hass, "/more-info/statistics/")}
|
||||
href=${documentationUrl(this.hass, this.helpMissingEntityUrl)}
|
||||
>${this.hass.localize(
|
||||
"ui.components.statistic-picker.learn_more"
|
||||
)}</a
|
||||
|
@ -112,9 +112,7 @@ export class StateBadge extends LitElement {
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
const iconStyle: { [name: string]: string } = {};
|
||||
const hostStyle: Partial<CSSStyleDeclaration> = {
|
||||
backgroundImage: "",
|
||||
};
|
||||
let backgroundImage = "";
|
||||
|
||||
this._showIcon = true;
|
||||
|
||||
@ -135,10 +133,12 @@ export class StateBadge extends LitElement {
|
||||
if (domain === "camera") {
|
||||
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
|
||||
}
|
||||
hostStyle.backgroundImage = `url(${imageUrl})`;
|
||||
backgroundImage = `url(${imageUrl})`;
|
||||
this._showIcon = false;
|
||||
if (domain === "update") {
|
||||
hostStyle.borderRadius = "0";
|
||||
this.style.borderRadius = "0";
|
||||
} else if (domain === "media_player") {
|
||||
this.style.borderRadius = "8%";
|
||||
}
|
||||
} else if (this.color) {
|
||||
// Externally provided overriding color wins over state color
|
||||
@ -179,12 +179,12 @@ export class StateBadge extends LitElement {
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
hostStyle.backgroundImage = `url(${imageUrl})`;
|
||||
backgroundImage = `url(${imageUrl})`;
|
||||
this._showIcon = false;
|
||||
}
|
||||
|
||||
this._iconStyle = iconStyle;
|
||||
Object.assign(this.style, hostStyle);
|
||||
this.style.backgroundImage = backgroundImage;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@ -95,25 +95,25 @@ class StateInfo extends LitElement {
|
||||
:host {
|
||||
min-width: 120px;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
state-badge {
|
||||
float: left;
|
||||
}
|
||||
:host([rtl]) state-badge {
|
||||
float: right;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 56px;
|
||||
margin-left: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:host([rtl]) .info {
|
||||
margin-right: 56px;
|
||||
margin-right: 8px;
|
||||
margin-left: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-area-picker";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { css, 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")>;
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-attribute-value")
|
||||
class HaAttributeValue extends LitElement {
|
||||
@ -44,7 +42,7 @@ class HaAttributeValue extends LitElement {
|
||||
${attributeValue}
|
||||
</a>
|
||||
`;
|
||||
} catch (_) {
|
||||
} catch {
|
||||
// Nothing to do here
|
||||
}
|
||||
}
|
||||
@ -55,15 +53,19 @@ class HaAttributeValue extends LitElement {
|
||||
attributeValue.some((val) => val instanceof Object)) ||
|
||||
(!Array.isArray(attributeValue) && attributeValue instanceof Object)
|
||||
) {
|
||||
if (!jsYamlPromise) {
|
||||
jsYamlPromise = import("../resources/js-yaml-dump");
|
||||
}
|
||||
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.dump(attributeValue));
|
||||
const yaml = import("js-yaml").then(({ dump }) => dump(attributeValue));
|
||||
return html`<pre>${until(yaml, "")}</pre>`;
|
||||
}
|
||||
|
||||
return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -26,6 +26,8 @@ export class HaButtonMenu extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public fixed = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-anchor" }) public noAnchor = false;
|
||||
|
||||
@query("mwc-menu", true) private _menu?: Menu;
|
||||
|
||||
public get items() {
|
||||
@ -82,7 +84,7 @@ export class HaButtonMenu extends LitElement {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this._menu!.anchor = this;
|
||||
this._menu!.anchor = this.noAnchor ? null : this;
|
||||
this._menu!.show();
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,9 @@ export class HaButton extends Button {
|
||||
.mdc-button {
|
||||
height: var(--button-height, 36px);
|
||||
}
|
||||
.trailing-icon {
|
||||
display: flex;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { CodeMirror, loadCodeMirror } from "../resources/codemirror.ondemand";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
|
||||
@ -58,7 +57,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
@state() private _value = "";
|
||||
|
||||
private _loadedCodeMirror?: CodeMirror;
|
||||
private _loadedCodeMirror?: typeof import("../resources/codemirror");
|
||||
|
||||
private _iconList?: Completion[];
|
||||
|
||||
@ -110,7 +109,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
// Ensure CodeMirror module is loaded before any update
|
||||
protected override async scheduleUpdate() {
|
||||
this._loadedCodeMirror ??= await loadCodeMirror();
|
||||
this._loadedCodeMirror ??= await import("../resources/codemirror");
|
||||
super.scheduleUpdate();
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,7 @@ class HaConfigEntryPicker extends LitElement {
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
|
@ -269,37 +269,61 @@ export class HaCountryPicker extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public countries?: string[];
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
private _getOptions = memoizeOne((language?: string) => {
|
||||
const countryDisplayNames =
|
||||
Intl && "DisplayNames" in Intl
|
||||
? new Intl.DisplayNames(language, {
|
||||
type: "region",
|
||||
fallback: "code",
|
||||
})
|
||||
: undefined;
|
||||
@property({ type: Boolean }) public noSort = false;
|
||||
|
||||
const options = COUNTRIES.map((country) => ({
|
||||
value: country,
|
||||
label: countryDisplayNames ? countryDisplayNames.of(country)! : country,
|
||||
}));
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.label, b.label, language)
|
||||
);
|
||||
return options;
|
||||
});
|
||||
private _getOptions = memoizeOne(
|
||||
(language?: string, countries?: string[]) => {
|
||||
let options: { label: string; value: string }[] = [];
|
||||
const countryDisplayNames =
|
||||
Intl && "DisplayNames" in Intl
|
||||
? new Intl.DisplayNames(language, {
|
||||
type: "region",
|
||||
fallback: "code",
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (countries) {
|
||||
options = countries.map((country) => ({
|
||||
value: country,
|
||||
label: countryDisplayNames
|
||||
? countryDisplayNames.of(country)!
|
||||
: country,
|
||||
}));
|
||||
} else {
|
||||
options = COUNTRIES.map((country) => ({
|
||||
value: country,
|
||||
label: countryDisplayNames
|
||||
? countryDisplayNames.of(country)!
|
||||
: country,
|
||||
}));
|
||||
}
|
||||
|
||||
if (!this.noSort) {
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.label, b.label, language)
|
||||
);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const options = this._getOptions(this.language);
|
||||
const options = this._getOptions(this.language, this.countries);
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
|
@ -15,10 +15,11 @@ import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { calcDate } from "../common/datetime/calc_date";
|
||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||
import { formatDate } from "../common/datetime/format_date";
|
||||
@ -29,6 +30,7 @@ import { HomeAssistant } from "../types";
|
||||
import "./date-range-picker";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import "./ha-icon-button";
|
||||
|
||||
export interface DateRangePickerRanges {
|
||||
[key: string]: [Date, Date];
|
||||
@ -54,8 +56,22 @@ export class HaDateRangePicker extends LitElement {
|
||||
|
||||
@property({ type: String }) private _rtlDirection = "ltr";
|
||||
|
||||
protected willUpdate() {
|
||||
if (!this.hasUpdated && this.ranges === undefined) {
|
||||
@property({ type: Boolean }) private minimal = false;
|
||||
|
||||
@property() public openingDirection?: "right" | "left" | "center" | "inline";
|
||||
|
||||
@state() private _calcedOpeningDirection?:
|
||||
| "right"
|
||||
| "left"
|
||||
| "center"
|
||||
| "inline";
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this.hasUpdated && this.ranges === undefined) ||
|
||||
(changedProps.has("hass") &&
|
||||
this.hass?.localize !== changedProps.get("hass")?.localize)
|
||||
) {
|
||||
const today = new Date();
|
||||
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
|
||||
const weekStart = calcDate(
|
||||
@ -133,41 +149,62 @@ export class HaDateRangePicker extends LitElement {
|
||||
<date-range-picker
|
||||
?disabled=${this.disabled}
|
||||
?auto-apply=${this.autoApply}
|
||||
?time-picker=${this.timePicker}
|
||||
time-picker=${this.timePicker}
|
||||
twentyfour-hours=${this._hour24format}
|
||||
start-date=${this.startDate}
|
||||
end-date=${this.endDate}
|
||||
?ranges=${this.ranges !== false}
|
||||
opening-direction=${this.openingDirection ||
|
||||
this._calcedOpeningDirection}
|
||||
first-day=${firstWeekdayIndex(this.hass.locale)}
|
||||
>
|
||||
<div slot="input" class="date-range-inputs">
|
||||
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
|
||||
<ha-textfield
|
||||
.value=${this.timePicker
|
||||
? formatDateTime(
|
||||
this.startDate,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)
|
||||
: formatDate(this.startDate, this.hass.locale, this.hass.config)}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.start_date"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._handleInputClick}
|
||||
readonly
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
.value=${this.timePicker
|
||||
? formatDateTime(this.endDate, this.hass.locale, this.hass.config)
|
||||
: formatDate(this.endDate, this.hass.locale, this.hass.config)}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.end_date"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._handleInputClick}
|
||||
readonly
|
||||
></ha-textfield>
|
||||
<div slot="input" class="date-range-inputs" @click=${this._handleClick}>
|
||||
${!this.minimal
|
||||
? html`<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
|
||||
<ha-textfield
|
||||
.value=${this.timePicker
|
||||
? formatDateTime(
|
||||
this.startDate,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)
|
||||
: formatDate(
|
||||
this.startDate,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.start_date"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._handleInputClick}
|
||||
readonly
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
.value=${this.timePicker
|
||||
? formatDateTime(
|
||||
this.endDate,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)
|
||||
: formatDate(
|
||||
this.endDate,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.end_date"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._handleInputClick}
|
||||
readonly
|
||||
></ha-textfield>`
|
||||
: html`<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.select_date_range"
|
||||
)}
|
||||
.path=${mdiCalendar}
|
||||
></ha-icon-button>`}
|
||||
</div>
|
||||
${this.ranges
|
||||
? html`<div
|
||||
@ -181,7 +218,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
)}
|
||||
</mwc-list>
|
||||
</div>`
|
||||
: ""}
|
||||
: nothing}
|
||||
<div slot="footer" class="date-range-footer">
|
||||
<mwc-button @click=${this._cancelDateRange}
|
||||
>${this.hass.localize("ui.common.cancel")}</mwc-button
|
||||
@ -225,6 +262,22 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleClick() {
|
||||
// calculate opening direction if not set
|
||||
if (!this._dateRangePicker.open && !this.openingDirection) {
|
||||
const datePickerPosition = this.getBoundingClientRect().x;
|
||||
let opens: "right" | "left" | "center" | "inline";
|
||||
if (datePickerPosition > (2 * window.innerWidth) / 3) {
|
||||
opens = "left";
|
||||
} else if (datePickerPosition < window.innerWidth / 3) {
|
||||
opens = "right";
|
||||
} else {
|
||||
opens = "center";
|
||||
}
|
||||
this._calcedOpeningDirection = opens;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-svg-icon {
|
||||
@ -234,6 +287,10 @@ export class HaDateRangePicker extends LitElement {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.date-range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -163,10 +163,7 @@ export class HaExpansionPanel extends LitElement {
|
||||
box-shadow: none;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(
|
||||
--ha-card-border-color,
|
||||
var(--divider-color, #e0e0e0)
|
||||
);
|
||||
border-color: var(--outline-color);
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,10 @@ export const computeInitialHaFormData = (
|
||||
typeof firstOption === "string" ? firstOption : firstOption.value;
|
||||
data[field.name] = selector.select.multiple ? [val] : val;
|
||||
}
|
||||
} else if ("country" in selector) {
|
||||
if (selector.country?.countries?.length) {
|
||||
data[field.name] = selector.country.countries[0];
|
||||
}
|
||||
} else if ("duration" in selector) {
|
||||
data[field.name] = {
|
||||
hours: 0,
|
||||
|
@ -57,8 +57,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
`
|
||||
: ""}
|
||||
<ha-slider
|
||||
pin
|
||||
ignore-bar-touch
|
||||
labeled
|
||||
.value=${this._value}
|
||||
.min=${this.schema.valueMin}
|
||||
.max=${this.schema.valueMax}
|
||||
|
@ -138,15 +138,13 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
}
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 12px;
|
||||
--mdc-icon-button-size: 24px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 12px;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@ -11,12 +11,12 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { CustomIcon, customIcons } from "../data/custom_icons";
|
||||
import {
|
||||
checkCacheVersion,
|
||||
Chunks,
|
||||
findIconChunk,
|
||||
getIcon,
|
||||
Icons,
|
||||
MDI_PREFIXES,
|
||||
checkCacheVersion,
|
||||
findIconChunk,
|
||||
getIcon,
|
||||
writeCache,
|
||||
} from "../data/iconsets";
|
||||
import "./ha-svg-icon";
|
||||
@ -47,6 +47,8 @@ export class HaIcon extends LitElement {
|
||||
|
||||
@state() private _path?: string;
|
||||
|
||||
@state() private _secondaryPath?: string;
|
||||
|
||||
@state() private _viewBox?: string;
|
||||
|
||||
@state() private _legacy = false;
|
||||
@ -55,6 +57,7 @@ export class HaIcon extends LitElement {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("icon")) {
|
||||
this._path = undefined;
|
||||
this._secondaryPath = undefined;
|
||||
this._viewBox = undefined;
|
||||
this._loadIcon();
|
||||
}
|
||||
@ -70,6 +73,7 @@ export class HaIcon extends LitElement {
|
||||
}
|
||||
return html`<ha-svg-icon
|
||||
.path=${this._path}
|
||||
.secondaryPath=${this._secondaryPath}
|
||||
.viewBox=${this._viewBox}
|
||||
></ha-svg-icon>`;
|
||||
}
|
||||
@ -175,6 +179,7 @@ export class HaIcon extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._path = icon.path;
|
||||
this._secondaryPath = icon.secondaryPath;
|
||||
this._viewBox = icon.viewBox;
|
||||
}
|
||||
|
||||
|
@ -1,90 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "./ha-icon";
|
||||
import "./ha-slider";
|
||||
|
||||
class HaLabeledSlider extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 5px 0 8px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
margin-top: 4px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-slider {
|
||||
flex-grow: 1;
|
||||
background-image: var(--ha-slider-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="title">[[_getTitle()]]</div>
|
||||
<div class="extra-container"><slot name="extra"></slot></div>
|
||||
<div class="slider-container">
|
||||
<ha-icon icon="[[icon]]" hidden$="[[!icon]]"></ha-icon>
|
||||
<ha-slider
|
||||
min="[[min]]"
|
||||
max="[[max]]"
|
||||
step="[[step]]"
|
||||
pin="[[pin]]"
|
||||
disabled="[[disabled]]"
|
||||
value="{{value}}"
|
||||
></ha-slider>
|
||||
</div>
|
||||
<template is="dom-if" if="[[helper]]">
|
||||
<ha-input-helper-text>[[helper]]</ha-input-helper-text>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
_getTitle() {
|
||||
return `${this.caption}${this.caption && this.required ? " *" : ""}`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
caption: String,
|
||||
disabled: Boolean,
|
||||
required: Boolean,
|
||||
min: Number,
|
||||
max: Number,
|
||||
pin: Boolean,
|
||||
step: Number,
|
||||
helper: String,
|
||||
|
||||
extra: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
ignoreBarTouch: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
notify: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-labeled-slider", HaLabeledSlider);
|
96
src/components/ha-labeled-slider.ts
Normal file
96
src/components/ha-labeled-slider.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-icon";
|
||||
import "./ha-slider";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-labeled-slider")
|
||||
class HaLabeledSlider extends LitElement {
|
||||
@property() public labeled? = false;
|
||||
|
||||
@property() public caption?: string;
|
||||
|
||||
@property() public disabled?: boolean;
|
||||
|
||||
@property() public required?: boolean;
|
||||
|
||||
@property() public min: number = 0;
|
||||
|
||||
@property() public max: number = 100;
|
||||
|
||||
@property() public step: number = 1;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public extra = false;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@property() public value?: number;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="title">${this._getTitle()}</div>
|
||||
<div class="extra-container"><slot name="extra"></slot></div>
|
||||
<div class="slider-container">
|
||||
${this.icon ? html`<ha-icon icon=${this.icon}></ha-icon>` : nothing}
|
||||
<ha-slider
|
||||
.min=${this.min}
|
||||
.max=${this.max}
|
||||
.step=${this.step}
|
||||
labeled=${this.labeled}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this.value}
|
||||
@change=${this._inputChanged}
|
||||
></ha-slider>
|
||||
</div>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text> ${this.helper} </ha-input-helper-text>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _getTitle(): string {
|
||||
return `${this.caption}${this.caption && this.required ? " *" : ""}`;
|
||||
}
|
||||
|
||||
private _inputChanged(ev) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: Number((ev.target as any).value),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 5px 0 8px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
margin-top: 8px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-slider {
|
||||
flex-grow: 1;
|
||||
background-image: var(--ha-slider-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-labeled-slider": HaLabeledSlider;
|
||||
}
|
||||
}
|
@ -1,19 +1,16 @@
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { OutlinedButton } from "@material/web/button/internal/outlined-button";
|
||||
import { styles as outlinedStyles } from "@material/web/button/internal/outlined-styles.css";
|
||||
import { styles as sharedStyles } from "@material/web/button/internal/shared-styles.css";
|
||||
import { MdOutlinedButton } from "@material/web/button/outlined-button";
|
||||
|
||||
@customElement("ha-outlined-button")
|
||||
export class HaOutlinedButton extends OutlinedButton {
|
||||
export class HaOutlinedButton extends MdOutlinedButton {
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
outlinedStyles,
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-outline: var(--divider-color);
|
||||
--md-sys-color-outline: var(--outline-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -1,21 +1,11 @@
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { IconButton } from "@material/web/iconbutton/internal/icon-button";
|
||||
import { styles as outlinedStyles } from "@material/web/iconbutton/internal/outlined-styles.css";
|
||||
import { styles as sharedStyles } from "@material/web/iconbutton/internal/shared-styles.css";
|
||||
import { MdOutlinedIconButton } from "@material/web/iconbutton/outlined-icon-button";
|
||||
|
||||
@customElement("ha-outlined-icon-button")
|
||||
export class HaOutlinedIconButton extends IconButton {
|
||||
protected override getRenderClasses() {
|
||||
return {
|
||||
...super.getRenderClasses(),
|
||||
outlined: true,
|
||||
};
|
||||
}
|
||||
|
||||
export class HaOutlinedIconButton extends MdOutlinedIconButton {
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
outlinedStyles,
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
|
@ -154,6 +154,8 @@ export class HaRelatedItems extends LitElement {
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=${entry.domain}
|
||||
slot="graphic"
|
||||
/>
|
||||
|
@ -1,15 +1,32 @@
|
||||
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||
import { styles } from "@material/mwc-select/mwc-select.css";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-select")
|
||||
export class HaSelect extends SelectBase {
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean }) public icon?: boolean;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
|
||||
|
||||
protected override render() {
|
||||
return html`
|
||||
${super.render()}
|
||||
${this.clearable && !this.required && !this.disabled && this.value
|
||||
? html`<ha-icon-button
|
||||
label="clear"
|
||||
@click=${this._clearValue}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
protected override renderLeadingIcon() {
|
||||
if (!this.icon) {
|
||||
return nothing;
|
||||
@ -33,6 +50,15 @@ export class HaSelect extends SelectBase {
|
||||
);
|
||||
}
|
||||
|
||||
private _clearValue(): void {
|
||||
if (this.disabled || !this.value) {
|
||||
return;
|
||||
}
|
||||
this.valueSetDirectly = true;
|
||||
this.select(-1);
|
||||
this.mdcFoundation.handleChange();
|
||||
}
|
||||
|
||||
private _translationsUpdated = debounce(async () => {
|
||||
await nextRender();
|
||||
this.layoutOptions();
|
||||
@ -41,6 +67,9 @@ export class HaSelect extends SelectBase {
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host([clearable]) {
|
||||
position: relative;
|
||||
}
|
||||
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
@ -68,6 +97,23 @@ export class HaSelect extends SelectBase {
|
||||
.mdc-select__anchor .mdc-floating-label--float-above {
|
||||
transform-origin: var(--float-start);
|
||||
}
|
||||
.mdc-select__selected-text-container {
|
||||
padding-inline-end: var(--select-selected-text-padding-end, 0px);
|
||||
}
|
||||
:host([clearable]) .mdc-select__selected-text-container {
|
||||
padding-inline-end: var(--select-selected-text-padding-end, 12px);
|
||||
}
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 28px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 28px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ export class HaColorTempSelector extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-labeled-slider
|
||||
pin
|
||||
labeled
|
||||
icon="hass:thermometer"
|
||||
.caption=${this.label || ""}
|
||||
.min=${this.selector.color_temp?.min_mireds ?? 153}
|
||||
@ -33,27 +33,25 @@ export class HaColorTempSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
@change=${this._valueChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-labeled-slider>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: Number((ev.target as any).value),
|
||||
value: Number((ev.detail as any).value),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-labeled-slider {
|
||||
--ha-slider-background: -webkit-linear-gradient(
|
||||
var(--float-end),
|
||||
--ha-slider-background: linear-gradient(
|
||||
to var(--float-end),
|
||||
rgb(255, 160, 0) 0%,
|
||||
white 50%,
|
||||
rgb(166, 209, 255) 100%
|
||||
);
|
||||
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
|
||||
--paper-slider-knob-start-border-color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
49
src/components/ha-selector/ha-selector-country.ts
Normal file
49
src/components/ha-selector/ha-selector-country.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { CountrySelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-country-picker";
|
||||
|
||||
@customElement("ha-selector-country")
|
||||
export class HaCountrySelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: CountrySelector;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-country-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.countries=${this.selector.country?.countries}
|
||||
.noSort=${this.selector.country?.no_sort}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-country-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-country-picker {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-country": HaCountrySelector;
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ export class HaDateSelector extends LitElement {
|
||||
.label=${this.label}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this.value}
|
||||
.value=${typeof this.value === "string" ? this.value : undefined}
|
||||
.required=${this.required}
|
||||
.helper=${this.helper}
|
||||
>
|
||||
|
@ -30,7 +30,8 @@ export class HaDateTimeSelector extends LitElement {
|
||||
@query("ha-time-input") private _timeInput!: HaTimeInput;
|
||||
|
||||
protected render() {
|
||||
const values = this.value?.split(" ");
|
||||
const values =
|
||||
typeof this.value === "string" ? this.value.split(" ") : undefined;
|
||||
|
||||
return html`
|
||||
<div class="input">
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@ -26,8 +26,22 @@ export class HaNumberSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
private _valueStr = "";
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (changedProps.has("value")) {
|
||||
if (this.value !== Number(this._valueStr)) {
|
||||
this._valueStr =
|
||||
!this.value || isNaN(this.value) ? "" : this.value.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const isBox = this.selector.number?.mode === "box";
|
||||
const isBox =
|
||||
this.selector.number?.mode === "box" ||
|
||||
this.selector.number?.min === undefined ||
|
||||
this.selector.number?.max === undefined;
|
||||
|
||||
return html`
|
||||
<div class="input">
|
||||
@ -37,6 +51,7 @@ export class HaNumberSelector extends LitElement {
|
||||
? html`${this.label}${this.required ? "*" : ""}`
|
||||
: ""}
|
||||
<ha-slider
|
||||
labeled
|
||||
.min=${this.selector.number?.min}
|
||||
.max=${this.selector.number?.max}
|
||||
.value=${this.value ?? ""}
|
||||
@ -45,8 +60,6 @@ export class HaNumberSelector extends LitElement {
|
||||
: this.selector.number?.step ?? 1}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
pin
|
||||
ignore-bar-touch
|
||||
@change=${this._handleSliderChange}
|
||||
>
|
||||
</ha-slider>
|
||||
@ -57,14 +70,12 @@ export class HaNumberSelector extends LitElement {
|
||||
(this.selector.number?.step ?? 1) % 1 !== 0
|
||||
? "decimal"
|
||||
: "numeric"}
|
||||
.label=${this.selector.number?.mode !== "box"
|
||||
? undefined
|
||||
: this.label}
|
||||
.label=${!isBox ? undefined : this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
class=${classMap({ single: this.selector.number?.mode === "box" })}
|
||||
class=${classMap({ single: isBox })}
|
||||
.min=${this.selector.number?.min}
|
||||
.max=${this.selector.number?.max}
|
||||
.value=${this.value ?? ""}
|
||||
.value=${this._valueStr ?? ""}
|
||||
.step=${this.selector.number?.step ?? 1}
|
||||
helperPersistent
|
||||
.helper=${isBox ? this.helper : undefined}
|
||||
@ -73,7 +84,7 @@ export class HaNumberSelector extends LitElement {
|
||||
.suffix=${this.selector.number?.unit_of_measurement}
|
||||
type="number"
|
||||
autoValidate
|
||||
?no-spinner=${this.selector.number?.mode !== "box"}
|
||||
?no-spinner=${!isBox}
|
||||
@input=${this._handleInputChange}
|
||||
>
|
||||
</ha-textfield>
|
||||
@ -86,6 +97,7 @@ export class HaNumberSelector extends LitElement {
|
||||
|
||||
private _handleInputChange(ev) {
|
||||
ev.stopPropagation();
|
||||
this._valueStr = ev.target.value;
|
||||
const value =
|
||||
ev.target.value === "" || isNaN(ev.target.value)
|
||||
? undefined
|
||||
|
@ -1,11 +1,16 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { mdiClose, mdiDrag } from "@mdi/js";
|
||||
import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { SortableEvent } from "sortablejs";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import type { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import { sortableStyles } from "../../resources/ha-sortable-style";
|
||||
import { SortableInstance } from "../../resources/sortable";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-checkbox";
|
||||
import "../ha-chip";
|
||||
@ -13,10 +18,9 @@ import "../ha-chip-set";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-formfield";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-radio";
|
||||
import "../ha-select";
|
||||
import "../ha-input-helper-text";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
|
||||
@customElement("ha-selector-select")
|
||||
export class HaSelectSelector extends LitElement {
|
||||
@ -38,6 +42,68 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
@query("ha-combo-box", true) private comboBox!: HaComboBox;
|
||||
|
||||
private _sortable?: SortableInstance;
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("value") || changedProps.has("selector")) {
|
||||
const sortableNeeded =
|
||||
this.selector.select?.multiple &&
|
||||
this.selector.select.reorder &&
|
||||
this.value?.length;
|
||||
if (!this._sortable && sortableNeeded) {
|
||||
this._createSortable();
|
||||
} else if (this._sortable && !sortableNeeded) {
|
||||
this._destroySortable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _createSortable() {
|
||||
const Sortable = (await import("../../resources/sortable")).default;
|
||||
this._sortable = new Sortable(
|
||||
this.shadowRoot!.querySelector("ha-chip-set")!,
|
||||
{
|
||||
animation: 150,
|
||||
fallbackClass: "sortable-fallback",
|
||||
draggable: "ha-chip",
|
||||
onChoose: (evt: SortableEvent) => {
|
||||
(evt.item as any).placeholder =
|
||||
document.createComment("sort-placeholder");
|
||||
evt.item.after((evt.item as any).placeholder);
|
||||
},
|
||||
onEnd: (evt: SortableEvent) => {
|
||||
// put back in original location
|
||||
if ((evt.item as any).placeholder) {
|
||||
(evt.item as any).placeholder.replaceWith(evt.item);
|
||||
delete (evt.item as any).placeholder;
|
||||
}
|
||||
this._dragged(evt);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _dragged(ev: SortableEvent): void {
|
||||
if (ev.oldIndex === ev.newIndex) return;
|
||||
this._move(ev.oldIndex!, ev.newIndex!);
|
||||
}
|
||||
|
||||
private _move(index: number, newIndex: number) {
|
||||
const value = this.value as string[];
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(index, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private _destroySortable() {
|
||||
this._sortable?.destroy();
|
||||
this._sortable = undefined;
|
||||
}
|
||||
|
||||
private _filter = "";
|
||||
|
||||
protected render() {
|
||||
@ -71,7 +137,11 @@ export class HaSelectSelector extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.selector.select?.custom_value && this._mode === "list") {
|
||||
if (
|
||||
!this.selector.select?.custom_value &&
|
||||
!this.selector.select?.reorder &&
|
||||
this._mode === "list"
|
||||
) {
|
||||
if (!this.selector.select?.multiple) {
|
||||
return html`
|
||||
<div>
|
||||
@ -124,23 +194,39 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
return html`
|
||||
${value?.length
|
||||
? html`<ha-chip-set>
|
||||
${value.map(
|
||||
(item, idx) => html`
|
||||
<ha-chip hasTrailingIcon>
|
||||
${options.find((option) => option.value === item)?.label ||
|
||||
item}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiClose}
|
||||
.idx=${idx}
|
||||
@click=${this._removeItem}
|
||||
></ha-svg-icon>
|
||||
</ha-chip>
|
||||
`
|
||||
)}
|
||||
</ha-chip-set>`
|
||||
: ""}
|
||||
? html`
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
value,
|
||||
(item) => item,
|
||||
(item, idx) => html`
|
||||
<ha-chip
|
||||
hasTrailingIcon
|
||||
.hasIcon=${this.selector.select?.reorder}
|
||||
>
|
||||
${this.selector.select?.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDrag}
|
||||
data-handle
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${options.find((option) => option.value === item)
|
||||
?.label || item}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiClose}
|
||||
.idx=${idx}
|
||||
@click=${this._removeItem}
|
||||
></ha-svg-icon>
|
||||
</ha-chip>
|
||||
`
|
||||
)}
|
||||
</ha-chip-set>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-combo-box
|
||||
item-value-path="value"
|
||||
@ -198,6 +284,7 @@ export class HaSelectSelector extends LitElement {
|
||||
.helper=${this.helper ?? ""}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
clearable
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueChanged}
|
||||
>
|
||||
@ -228,7 +315,7 @@ export class HaSelectSelector extends LitElement {
|
||||
private _valueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail?.value || ev.target.value;
|
||||
if (this.disabled || value === undefined || value === this.value) {
|
||||
if (this.disabled || value === undefined || value === (this.value ?? "")) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
@ -330,16 +417,22 @@ export class HaSelectSelector extends LitElement {
|
||||
this.comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-select,
|
||||
mwc-formfield,
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
mwc-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
`;
|
||||
static styles = [
|
||||
sortableStyles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
ha-select,
|
||||
mwc-formfield,
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
mwc-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -111,13 +111,13 @@ export class HaTextSelector extends LitElement {
|
||||
}
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 10px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
|
@ -23,7 +23,7 @@ export class HaTimeSelector extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-time-input
|
||||
.value=${this.value}
|
||||
.value=${typeof this.value === "string" ? this.value : undefined}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
|
@ -21,6 +21,7 @@ const LOAD_ELEMENTS = {
|
||||
config_entry: () => import("./ha-selector-config-entry"),
|
||||
conversation_agent: () => import("./ha-selector-conversation-agent"),
|
||||
constant: () => import("./ha-selector-constant"),
|
||||
country: () => import("./ha-selector-country"),
|
||||
date: () => import("./ha-selector-date"),
|
||||
datetime: () => import("./ha-selector-datetime"),
|
||||
device: () => import("./ha-selector-device"),
|
||||
|
@ -2,9 +2,9 @@ import "@material/mwc-button/mwc-button";
|
||||
import {
|
||||
mdiBell,
|
||||
mdiCalendar,
|
||||
mdiCart,
|
||||
mdiCellphoneCog,
|
||||
mdiChartBox,
|
||||
mdiClipboardList,
|
||||
mdiClose,
|
||||
mdiCog,
|
||||
mdiFormatListBulletedType,
|
||||
@ -50,7 +50,7 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
import { UpdateEntity, updateCanInstall } from "../data/update";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { SortableInstance, loadSortable } from "../resources/sortable.ondemand";
|
||||
import type { SortableInstance } from "../resources/sortable";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
import "./ha-icon";
|
||||
@ -81,7 +81,7 @@ const PANEL_ICONS = {
|
||||
lovelace: mdiViewDashboard,
|
||||
map: mdiTooltipAccount,
|
||||
"media-browser": mdiPlayBoxMultiple,
|
||||
"shopping-list": mdiCart,
|
||||
todo: mdiClipboardList,
|
||||
};
|
||||
|
||||
const panelSorter = (
|
||||
@ -689,7 +689,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _createSortable() {
|
||||
const Sortable = await loadSortable();
|
||||
const Sortable = (await import("../resources/sortable")).default;
|
||||
this._sortable = new Sortable(
|
||||
this.shadowRoot!.getElementById("sortable")!,
|
||||
{
|
||||
|
@ -1,117 +0,0 @@
|
||||
import "@polymer/paper-slider";
|
||||
|
||||
const PaperSliderClass = customElements.get("paper-slider");
|
||||
let subTemplate;
|
||||
|
||||
export class HaSlider extends PaperSliderClass {
|
||||
static get template() {
|
||||
if (!subTemplate) {
|
||||
subTemplate = PaperSliderClass.template.cloneNode(true);
|
||||
|
||||
const superStyle = subTemplate.content.querySelector("style");
|
||||
|
||||
// append style to add mirroring of pin in RTL
|
||||
superStyle.appendChild(
|
||||
document.createTextNode(`
|
||||
:host([dir="rtl"]) #sliderContainer.pin.expand > .slider-knob > .slider-knob-inner::after {
|
||||
-webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important;
|
||||
transform: scale(1) translate(0, -17px) scaleX(-1) !important;
|
||||
}
|
||||
|
||||
.pin > .slider-knob > .slider-knob-inner {
|
||||
font-size: var(--ha-slider-pin-font-size, 15px);
|
||||
line-height: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled.ring > .slider-knob > .slider-knob-inner {
|
||||
background-color: var(--paper-slider-disabled-knob-color, var(--disabled-text-color));
|
||||
border: 2px solid var(--paper-slider-disabled-knob-color, var(--disabled-text-color));
|
||||
}
|
||||
|
||||
.pin > .slider-knob > .slider-knob-inner::before {
|
||||
top: unset;
|
||||
margin-left: unset;
|
||||
|
||||
bottom: calc(15px + var(--calculated-paper-slider-height)/2);
|
||||
left: 50%;
|
||||
width: 2.6em;
|
||||
height: 2.6em;
|
||||
|
||||
-webkit-transform-origin: left bottom;
|
||||
transform-origin: left bottom;
|
||||
-webkit-transform: rotate(-45deg) scale(0) translate(0);
|
||||
transform: rotate(-45deg) scale(0) translate(0);
|
||||
}
|
||||
|
||||
.pin.expand > .slider-knob > .slider-knob-inner::before {
|
||||
-webkit-transform: rotate(-45deg) scale(1) translate(7px, -7px);
|
||||
transform: rotate(-45deg) scale(1) translate(7px, -7px);
|
||||
}
|
||||
|
||||
.pin > .slider-knob > .slider-knob-inner::after {
|
||||
top: unset;
|
||||
font-size: unset;
|
||||
|
||||
bottom: calc(15px + var(--calculated-paper-slider-height)/2);
|
||||
left: 50%;
|
||||
margin-left: -1.3em;
|
||||
width: 2.6em;
|
||||
height: 2.5em;
|
||||
|
||||
-webkit-transform-origin: center bottom;
|
||||
transform-origin: center bottom;
|
||||
-webkit-transform: scale(0) translate(0);
|
||||
transform: scale(0) translate(0);
|
||||
}
|
||||
|
||||
.pin.expand > .slider-knob > .slider-knob-inner::after {
|
||||
-webkit-transform: scale(1) translate(0, -10px);
|
||||
transform: scale(1) translate(0, -10px);
|
||||
}
|
||||
|
||||
.slider-input {
|
||||
width: 54px;
|
||||
}
|
||||
`)
|
||||
);
|
||||
}
|
||||
return subTemplate;
|
||||
}
|
||||
|
||||
_setImmediateValue(newImmediateValue) {
|
||||
super._setImmediateValue(
|
||||
this.step >= 1
|
||||
? Math.round(newImmediateValue)
|
||||
: Math.round(newImmediateValue * 100) / 100
|
||||
);
|
||||
}
|
||||
|
||||
_calcStep(value) {
|
||||
if (!this.step) {
|
||||
return parseFloat(value);
|
||||
}
|
||||
|
||||
const numSteps = Math.round((value - this.min) / this.step);
|
||||
const stepStr = this.step.toString();
|
||||
const stepPointAt = stepStr.indexOf(".");
|
||||
if (stepPointAt !== -1) {
|
||||
/**
|
||||
* For small values of this.step, if we calculate the step using
|
||||
* For non-integer values of this.step, if we calculate the step using
|
||||
* `Math.round(value / step) * step` we may hit a precision point issue
|
||||
* eg. 0.1 * 0.2 = 0.020000000000000004
|
||||
* http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
|
||||
*
|
||||
* as a work around we can round with the decimal precision of `step`
|
||||
*/
|
||||
const precision = 10 ** (stepStr.length - stepPointAt - 1);
|
||||
return (
|
||||
Math.round((numSteps * this.step + this.min) * precision) / precision
|
||||
);
|
||||
}
|
||||
|
||||
return numSteps * this.step + this.min;
|
||||
}
|
||||
}
|
||||
customElements.define("ha-slider", HaSlider);
|
26
src/components/ha-slider.ts
Normal file
26
src/components/ha-slider.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import { MdSlider } from "@material/web/slider/slider";
|
||||
import { CSSResult, css } from "lit";
|
||||
|
||||
@customElement("ha-slider")
|
||||
export class HaSlider extends MdSlider {
|
||||
static override styles: CSSResult[] = [
|
||||
...MdSlider.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-primary: var(--primary-color);
|
||||
--md-sys-color-outline: var(--outline-color);
|
||||
|
||||
min-width: 100px;
|
||||
min-inline-size: 100px;
|
||||
width: 200px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-slider": HaSlider;
|
||||
}
|
||||
}
|
@ -1,10 +1,19 @@
|
||||
import { css, CSSResultGroup, LitElement, svg, SVGTemplateResult } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
nothing,
|
||||
svg,
|
||||
SVGTemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-svg-icon")
|
||||
export class HaSvgIcon extends LitElement {
|
||||
@property() public path?: string;
|
||||
|
||||
@property() public secondaryPath?: string;
|
||||
|
||||
@property() public viewBox?: string;
|
||||
|
||||
protected render(): SVGTemplateResult {
|
||||
@ -13,11 +22,20 @@ export class HaSvgIcon extends LitElement {
|
||||
viewBox=${this.viewBox || "0 0 24 24"}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false"
|
||||
role="img"
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g>
|
||||
${this.path ? svg`<path d=${this.path}></path>` : ""}
|
||||
${
|
||||
this.path
|
||||
? svg`<path class="primary-path" d=${this.path}></path>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.secondaryPath
|
||||
? svg`<path class="secondary-path" d=${this.secondaryPath}></path>`
|
||||
: nothing
|
||||
}
|
||||
</g>
|
||||
</svg>`;
|
||||
}
|
||||
@ -30,7 +48,7 @@ export class HaSvgIcon extends LitElement {
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
fill: currentcolor;
|
||||
fill: var(--icon-primary-color, currentcolor);
|
||||
width: var(--mdc-icon-size, 24px);
|
||||
height: var(--mdc-icon-size, 24px);
|
||||
}
|
||||
@ -40,6 +58,13 @@ export class HaSvgIcon extends LitElement {
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
}
|
||||
path.primary-path {
|
||||
opacity: var(--icon-primary-opactity, 1);
|
||||
}
|
||||
path.secondary-path {
|
||||
fill: var(--icon-secondary-color, currentcolor);
|
||||
opacity: var(--icon-secondary-opactity, 0.5);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
320
src/components/ha-two-pane-top-app-bar-fixed.ts
Normal file
320
src/components/ha-two-pane-top-app-bar-fixed.ts
Normal file
@ -0,0 +1,320 @@
|
||||
import {
|
||||
addHasRemoveClass,
|
||||
BaseElement,
|
||||
} from "@material/mwc-base/base-element";
|
||||
import { supportsPassiveEventListener } from "@material/mwc-base/utils";
|
||||
import { MDCTopAppBarAdapter } from "@material/top-app-bar/adapter";
|
||||
import { strings } from "@material/top-app-bar/constants";
|
||||
import MDCFixedTopAppBarFoundation from "@material/top-app-bar/fixed/foundation";
|
||||
import { html, css, nothing } from "lit";
|
||||
import { property, query, customElement } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
|
||||
export const passiveEventOptionsIfSupported = supportsPassiveEventListener
|
||||
? { passive: true }
|
||||
: undefined;
|
||||
|
||||
@customElement("ha-two-pane-top-app-bar-fixed")
|
||||
export abstract class TopAppBarBaseBase extends BaseElement {
|
||||
protected override mdcFoundation!: MDCFixedTopAppBarFoundation;
|
||||
|
||||
protected override mdcFoundationClass = MDCFixedTopAppBarFoundation;
|
||||
|
||||
@query(".mdc-top-app-bar") protected mdcRoot!: HTMLElement;
|
||||
|
||||
// _actionItemsSlot should have type HTMLSlotElement, but when TypeScript's
|
||||
// emitDecoratorMetadata is enabled, the HTMLSlotElement constructor will
|
||||
// be emitted into the runtime, which will cause an "HTMLSlotElement is
|
||||
// undefined" error in browsers that don't define it (e.g. IE11).
|
||||
@query('slot[name="actionItems"]') protected _actionItemsSlot!: HTMLElement;
|
||||
|
||||
protected _scrollTarget!: HTMLElement | Window;
|
||||
|
||||
@property({ type: Boolean }) centerTitle = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) prominent = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) dense = false;
|
||||
|
||||
@property({ type: Boolean }) pane = false;
|
||||
|
||||
@property({ type: Boolean }) footer = false;
|
||||
|
||||
@query(".content") private _contentElement!: HTMLElement;
|
||||
|
||||
@query(".pane .ha-scrollbar") private _paneElement?: HTMLElement;
|
||||
|
||||
@property({ type: Object })
|
||||
get scrollTarget() {
|
||||
return this._scrollTarget || window;
|
||||
}
|
||||
|
||||
set scrollTarget(value) {
|
||||
this.unregisterListeners();
|
||||
const old = this.scrollTarget;
|
||||
this._scrollTarget = value;
|
||||
this.updateRootPosition();
|
||||
this.requestUpdate("scrollTarget", old);
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
protected updateRootPosition() {
|
||||
if (this.mdcRoot) {
|
||||
const windowScroller = this.scrollTarget === window;
|
||||
// we add support for top-app-bar's tied to an element scroller.
|
||||
this.mdcRoot.style.position = windowScroller ? "" : "absolute";
|
||||
}
|
||||
}
|
||||
|
||||
protected barClasses() {
|
||||
return {
|
||||
"mdc-top-app-bar--dense": this.dense,
|
||||
"mdc-top-app-bar--prominent": this.prominent,
|
||||
"center-title": this.centerTitle,
|
||||
"mdc-top-app-bar--fixed": true,
|
||||
"mdc-top-app-bar--pane": this.pane,
|
||||
};
|
||||
}
|
||||
|
||||
protected contentClasses() {
|
||||
return {
|
||||
"mdc-top-app-bar--fixed-adjust": !this.dense && !this.prominent,
|
||||
"mdc-top-app-bar--dense-fixed-adjust": this.dense && !this.prominent,
|
||||
"mdc-top-app-bar--prominent-fixed-adjust": !this.dense && this.prominent,
|
||||
"mdc-top-app-bar--dense-prominent-fixed-adjust":
|
||||
this.dense && this.prominent,
|
||||
"mdc-top-app-bar--pane": this.pane,
|
||||
};
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
const title = html`<span class="mdc-top-app-bar__title"
|
||||
><slot name="title"></slot
|
||||
></span>`;
|
||||
return html`
|
||||
<header class="mdc-top-app-bar ${classMap(this.barClasses())}">
|
||||
<div class="mdc-top-app-bar__row">
|
||||
${this.pane
|
||||
? html`<section
|
||||
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start"
|
||||
id="title"
|
||||
>
|
||||
<slot
|
||||
name="navigationIcon"
|
||||
@click=${this.handleNavigationClick}
|
||||
></slot>
|
||||
${title}
|
||||
</section>`
|
||||
: nothing}
|
||||
<section class="mdc-top-app-bar__section" id="navigation">
|
||||
${this.pane
|
||||
? nothing
|
||||
: html`<slot
|
||||
name="navigationIcon"
|
||||
@click=${this.handleNavigationClick}
|
||||
></slot
|
||||
>${title}`}
|
||||
</section>
|
||||
<section
|
||||
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-end"
|
||||
id="actions"
|
||||
role="toolbar"
|
||||
>
|
||||
<slot name="actionItems"></slot>
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
<div class=${classMap(this.contentClasses())}>
|
||||
${this.pane
|
||||
? html`<div class="pane">
|
||||
<div class="shadow-container"></div>
|
||||
<div class="ha-scrollbar">
|
||||
<slot name="pane"></slot>
|
||||
</div>
|
||||
${this.footer
|
||||
? html`<div class="footer">
|
||||
<slot name="pane-footer"></slot>
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="main">
|
||||
${this.pane ? html`<div class="shadow-container"></div>` : nothing}
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("pane") &&
|
||||
changedProperties.get("pane") !== undefined
|
||||
) {
|
||||
this.unregisterListeners();
|
||||
this.registerListeners();
|
||||
}
|
||||
}
|
||||
|
||||
protected createAdapter(): MDCTopAppBarAdapter {
|
||||
return {
|
||||
...addHasRemoveClass(this.mdcRoot),
|
||||
setStyle: (prprty: string, value: string) =>
|
||||
this.mdcRoot.style.setProperty(prprty, value),
|
||||
getTopAppBarHeight: () => this.mdcRoot.clientHeight,
|
||||
notifyNavigationIconClicked: () => {
|
||||
this.dispatchEvent(
|
||||
new Event(strings.NAVIGATION_EVENT, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
getViewportScrollY: () =>
|
||||
this.scrollTarget instanceof Window
|
||||
? this.scrollTarget.pageYOffset
|
||||
: this.scrollTarget.scrollTop,
|
||||
getTotalActionItems: () =>
|
||||
(this._actionItemsSlot as HTMLSlotElement).assignedNodes({
|
||||
flatten: true,
|
||||
}).length,
|
||||
};
|
||||
}
|
||||
|
||||
protected handleTargetScroll = () => {
|
||||
this.mdcFoundation.handleTargetScroll();
|
||||
};
|
||||
|
||||
protected handlePaneScroll = (ev) => {
|
||||
if (ev.target.scrollTop > 0) {
|
||||
ev.target.parentElement.classList.add("scrolled");
|
||||
} else {
|
||||
ev.target.parentElement.classList.remove("scrolled");
|
||||
}
|
||||
};
|
||||
|
||||
protected handleNavigationClick = () => {
|
||||
this.mdcFoundation.handleNavigationClick();
|
||||
};
|
||||
|
||||
protected registerListeners() {
|
||||
if (this.pane) {
|
||||
this._paneElement!.addEventListener(
|
||||
"scroll",
|
||||
this.handlePaneScroll,
|
||||
passiveEventOptionsIfSupported
|
||||
);
|
||||
this._contentElement.addEventListener(
|
||||
"scroll",
|
||||
this.handlePaneScroll,
|
||||
passiveEventOptionsIfSupported
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.scrollTarget.addEventListener(
|
||||
"scroll",
|
||||
this.handleTargetScroll,
|
||||
passiveEventOptionsIfSupported
|
||||
);
|
||||
}
|
||||
|
||||
protected unregisterListeners() {
|
||||
this._paneElement?.removeEventListener("scroll", this.handlePaneScroll);
|
||||
this._contentElement.removeEventListener("scroll", this.handlePaneScroll);
|
||||
this.scrollTarget.removeEventListener("scroll", this.handleTargetScroll);
|
||||
}
|
||||
|
||||
protected override firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this.updateRootPosition();
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.unregisterListeners();
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
.mdc-top-app-bar__row {
|
||||
height: var(--header-height);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
.mdc-top-app-bar--fixed-adjust {
|
||||
padding-top: var(--header-height);
|
||||
}
|
||||
.shadow-container {
|
||||
position: absolute;
|
||||
top: calc(-1 * var(--header-height));
|
||||
width: 100%;
|
||||
height: var(--header-height);
|
||||
z-index: 1;
|
||||
transition: box-shadow 200ms linear;
|
||||
}
|
||||
.scrolled .shadow-container {
|
||||
box-shadow: var(
|
||||
--mdc-top-app-bar-fixed-box-shadow,
|
||||
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
|
||||
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
|
||||
);
|
||||
}
|
||||
.mdc-top-app-bar {
|
||||
--mdc-typography-headline6-font-weight: 400;
|
||||
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
|
||||
background-color: var(
|
||||
--app-header-background-color,
|
||||
var(--mdc-theme-primary)
|
||||
);
|
||||
}
|
||||
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
|
||||
box-shadow: none;
|
||||
}
|
||||
#title {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.12);
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 var(--sidepane-width, 250px);
|
||||
width: var(--sidepane-width, 250px);
|
||||
}
|
||||
div.mdc-top-app-bar--pane {
|
||||
display: flex;
|
||||
height: calc(100vh - var(--header-height));
|
||||
}
|
||||
.pane {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 0 0 var(--sidepane-width, 250px);
|
||||
width: var(--sidepane-width, 250px);
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
.pane .ha-scrollbar {
|
||||
flex: 1;
|
||||
}
|
||||
.pane .footer {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
.main {
|
||||
min-height: 100%;
|
||||
}
|
||||
.mdc-top-app-bar--pane .main {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
.mdc-top-app-bar--pane .content {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
@ -298,7 +298,7 @@ export class HatScriptGraph extends LitElement {
|
||||
.notEnabled=${disabled || config.enabled === false}
|
||||
nofocus
|
||||
></hat-graph-node>
|
||||
${ensureArray(config.then).map((action, j) =>
|
||||
${ensureArray(config.then ?? []).map((action, j) =>
|
||||
this.render_action_node(
|
||||
action,
|
||||
`${path}/then/${j}`,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user