mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
20230830.0 (#17737)
This commit is contained in:
commit
96597b3963
8
.github/workflows/cast_deployment.yaml
vendored
8
.github/workflows/cast_deployment.yaml
vendored
@ -21,12 +21,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.7.0
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -57,12 +57,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.7.0
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
16
.github/workflows/ci.yaml
vendored
16
.github/workflows/ci.yaml
vendored
@ -24,9 +24,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.7.0
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -55,9 +55,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.7.0
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -73,9 +73,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.7.0
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -91,9 +91,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.7.0
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
|
8
.github/workflows/demo_deployment.yaml
vendored
8
.github/workflows/demo_deployment.yaml
vendored
@ -22,12 +22,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.7.0
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -58,12 +58,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.7.0
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
4
.github/workflows/design_deployment.yaml
vendored
4
.github/workflows/design_deployment.yaml
vendored
@ -16,10 +16,10 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.7.0
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
4
.github/workflows/design_preview.yaml
vendored
4
.github/workflows/design_preview.yaml
vendored
@ -21,10 +21,10 @@ jobs:
|
|||||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.7.0
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@ -20,7 +20,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@ -28,7 +28,7 @@ jobs:
|
|||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.7.0
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
|
|
||||||
- name: Verify version
|
- name: Verify version
|
||||||
uses: home-assistant/actions/helpers/verify-version@master
|
uses: home-assistant/actions/helpers/verify-version@master
|
||||||
@ -34,7 +34,7 @@ jobs:
|
|||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.7.0
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v3.6.0
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
run: |
|
run: |
|
||||||
|
File diff suppressed because one or more lines are too long
@ -8,4 +8,4 @@ plugins:
|
|||||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||||
spec: "@yarnpkg/plugin-interactive-tools"
|
spec: "@yarnpkg/plugin-interactive-tools"
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-3.6.1.cjs
|
yarnPath: .yarn/releases/yarn-3.6.3.cjs
|
||||||
|
@ -8,7 +8,7 @@ module.exports.sourceMapURL = () => {
|
|||||||
const ref = env.version().endsWith("dev")
|
const ref = env.version().endsWith("dev")
|
||||||
? process.env.GITHUB_SHA || "dev"
|
? process.env.GITHUB_SHA || "dev"
|
||||||
: env.version();
|
: env.version();
|
||||||
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}`;
|
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}/`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Files from NPM Packages that should not be imported
|
// Files from NPM Packages that should not be imported
|
||||||
@ -98,7 +98,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
|||||||
"@babel/preset-env",
|
"@babel/preset-env",
|
||||||
{
|
{
|
||||||
useBuiltIns: latestBuild ? false : "entry",
|
useBuiltIns: latestBuild ? false : "entry",
|
||||||
corejs: latestBuild ? false : { version: "3.31", proposals: true },
|
corejs: latestBuild ? false : { version: "3.32", proposals: true },
|
||||||
bugfixes: true,
|
bugfixes: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -2,44 +2,15 @@
|
|||||||
|
|
||||||
import gulp from "gulp";
|
import gulp from "gulp";
|
||||||
import zopfli from "gulp-zopfli-green";
|
import zopfli from "gulp-zopfli-green";
|
||||||
import merge from "merge-stream";
|
|
||||||
import path from "path";
|
|
||||||
import paths from "../paths.cjs";
|
import paths from "../paths.cjs";
|
||||||
|
|
||||||
const zopfliOptions = { threshold: 150 };
|
const zopfliOptions = { threshold: 150 };
|
||||||
|
|
||||||
gulp.task("compress-app", function compressApp() {
|
const compressDist = (rootDir) =>
|
||||||
const jsLatest = gulp
|
gulp
|
||||||
.src(path.resolve(paths.app_output_latest, "**/*.js"))
|
.src([`${rootDir}/**/*.{js,json,css,svg}`])
|
||||||
.pipe(zopfli(zopfliOptions))
|
.pipe(zopfli(zopfliOptions))
|
||||||
.pipe(gulp.dest(paths.app_output_latest));
|
.pipe(gulp.dest(rootDir));
|
||||||
|
|
||||||
const jsEs5 = gulp
|
gulp.task("compress-app", () => compressDist(paths.app_output_root));
|
||||||
.src(path.resolve(paths.app_output_es5, "**/*.js"))
|
gulp.task("compress-hassio", () => compressDist(paths.hassio_output_root));
|
||||||
.pipe(zopfli(zopfliOptions))
|
|
||||||
.pipe(gulp.dest(paths.app_output_es5));
|
|
||||||
|
|
||||||
const polyfills = gulp
|
|
||||||
.src(path.resolve(paths.app_output_static, "polyfills/*.js"))
|
|
||||||
.pipe(zopfli(zopfliOptions))
|
|
||||||
.pipe(gulp.dest(path.resolve(paths.app_output_static, "polyfills")));
|
|
||||||
|
|
||||||
const translations = gulp
|
|
||||||
.src(path.resolve(paths.app_output_static, "translations/**/*.json"))
|
|
||||||
.pipe(zopfli(zopfliOptions))
|
|
||||||
.pipe(gulp.dest(path.resolve(paths.app_output_static, "translations")));
|
|
||||||
|
|
||||||
const icons = gulp
|
|
||||||
.src(path.resolve(paths.app_output_static, "mdi/*.json"))
|
|
||||||
.pipe(zopfli(zopfliOptions))
|
|
||||||
.pipe(gulp.dest(path.resolve(paths.app_output_static, "mdi")));
|
|
||||||
|
|
||||||
return merge(jsLatest, jsEs5, polyfills, translations, icons);
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task("compress-hassio", function compressApp() {
|
|
||||||
return gulp
|
|
||||||
.src(path.resolve(paths.hassio_output_root, "**/*.js"))
|
|
||||||
.pipe(zopfli(zopfliOptions))
|
|
||||||
.pipe(gulp.dest(paths.hassio_output_root));
|
|
||||||
});
|
|
||||||
|
@ -1,18 +1,12 @@
|
|||||||
import { deleteSync } from "del";
|
import { deleteSync } from "del";
|
||||||
import fs from "fs";
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
||||||
import gulp from "gulp";
|
import gulp from "gulp";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import paths from "../paths.cjs";
|
import paths from "../paths.cjs";
|
||||||
|
|
||||||
const outDir = "build/locale-data";
|
const outDir = path.join(paths.build_dir, "locale-data");
|
||||||
|
|
||||||
gulp.task("clean-locale-data", async () => deleteSync([outDir]));
|
const INTL_PACKAGES = {
|
||||||
|
|
||||||
gulp.task("ensure-locale-data-build-dir", async () => {
|
|
||||||
fs.mkdirSync(outDir, { recursive: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
const modules = {
|
|
||||||
"intl-relativetimeformat": "RelativeTimeFormat",
|
"intl-relativetimeformat": "RelativeTimeFormat",
|
||||||
"intl-datetimeformat": "DateTimeFormat",
|
"intl-datetimeformat": "DateTimeFormat",
|
||||||
"intl-numberformat": "NumberFormat",
|
"intl-numberformat": "NumberFormat",
|
||||||
@ -20,53 +14,60 @@ const modules = {
|
|||||||
"intl-listformat": "ListFormat",
|
"intl-listformat": "ListFormat",
|
||||||
};
|
};
|
||||||
|
|
||||||
gulp.task("create-locale-data", (done) => {
|
const convertToJSON = async (pkg, lang) => {
|
||||||
|
let localeData;
|
||||||
|
try {
|
||||||
|
localeData = await readFile(
|
||||||
|
path.resolve(
|
||||||
|
paths.polymer_dir,
|
||||||
|
`node_modules/@formatjs/${pkg}/locale-data/${lang}.js`
|
||||||
|
),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore if language is missing (i.e. not supported by @formatjs)
|
||||||
|
if (e.code === "ENOENT") {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
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, "");
|
||||||
|
// Parse to validate JSON, then stringify to minify
|
||||||
|
localeData = JSON.stringify(JSON.parse(localeData));
|
||||||
|
await writeFile(path.join(outDir, `${pkg}/${lang}.json`), localeData);
|
||||||
|
};
|
||||||
|
|
||||||
|
gulp.task("clean-locale-data", async () => deleteSync([outDir]));
|
||||||
|
|
||||||
|
gulp.task("create-locale-data", async () => {
|
||||||
const translationMeta = JSON.parse(
|
const translationMeta = JSON.parse(
|
||||||
fs.readFileSync(
|
await readFile(
|
||||||
path.join(paths.translations_src, "translationMetadata.json")
|
path.resolve(paths.translations_src, "translationMetadata.json"),
|
||||||
|
"utf-8"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
Object.entries(modules).forEach(([module, className]) => {
|
const conversions = [];
|
||||||
Object.keys(translationMeta).forEach((lang) => {
|
for (const pkg of Object.keys(INTL_PACKAGES)) {
|
||||||
try {
|
await mkdir(path.join(outDir, pkg), { recursive: true });
|
||||||
const localeData = fs
|
for (const lang of Object.keys(translationMeta)) {
|
||||||
.readFileSync(
|
conversions.push(convertToJSON(pkg, lang));
|
||||||
path.resolve(
|
}
|
||||||
paths.polymer_dir,
|
}
|
||||||
`node_modules/@formatjs/${module}/locale-data/${lang}.js`
|
await Promise.all(conversions);
|
||||||
),
|
|
||||||
"utf-8"
|
|
||||||
)
|
|
||||||
.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, "");
|
|
||||||
// make sure we have valid JSON
|
|
||||||
JSON.parse(localeData);
|
|
||||||
fs.mkdirSync(path.join(outDir, module), { recursive: true });
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(outDir, `${module}/${lang}.json`),
|
|
||||||
localeData
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.code !== "ENOENT") {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"build-locale-data",
|
"build-locale-data",
|
||||||
gulp.series(
|
gulp.series("clean-locale-data", "create-locale-data")
|
||||||
"clean-locale-data",
|
|
||||||
"ensure-locale-data-build-dir",
|
|
||||||
"create-locale-data"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
@ -415,7 +415,7 @@ gulp.task("build-translation-write-metadata", () =>
|
|||||||
gulp.task(
|
gulp.task(
|
||||||
"create-translations",
|
"create-translations",
|
||||||
gulp.series(
|
gulp.series(
|
||||||
env.isProdBuild() ? (done) => done() : "create-test-translation",
|
...(env.isProdBuild() ? [] : ["create-test-translation"]),
|
||||||
"build-master-translation",
|
"build-master-translation",
|
||||||
"build-merged-translations",
|
"build-merged-translations",
|
||||||
gulp.parallel(...splitTasks),
|
gulp.parallel(...splitTasks),
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
// Tasks to run webpack.
|
// Tasks to run webpack.
|
||||||
|
|
||||||
import log from "fancy-log";
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import gulp from "gulp";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import log from "fancy-log";
|
||||||
|
import gulp from "gulp";
|
||||||
import webpack from "webpack";
|
import webpack from "webpack";
|
||||||
import WebpackDevServer from "webpack-dev-server";
|
import WebpackDevServer from "webpack-dev-server";
|
||||||
import env from "../env.cjs";
|
import env from "../env.cjs";
|
||||||
@ -44,6 +44,7 @@ const runDevServer = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const server = new WebpackDevServer(
|
const server = new WebpackDevServer(
|
||||||
{
|
{
|
||||||
|
hot: false,
|
||||||
open: true,
|
open: true,
|
||||||
host: listenHost,
|
host: listenHost,
|
||||||
port,
|
port,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const webpack = require("webpack");
|
const { existsSync } = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const webpack = require("webpack");
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||||
const log = require("fancy-log");
|
const log = require("fancy-log");
|
||||||
@ -191,19 +192,26 @@ const createWebpackConfig = ({
|
|||||||
// Since production source maps don't include sources, we need to point to them elsewhere
|
// Since production source maps don't include sources, we need to point to them elsewhere
|
||||||
// For dependencies, just provide the path (no source in browser)
|
// For dependencies, just provide the path (no source in browser)
|
||||||
// Otherwise, point to the raw code on GitHub for browser to load
|
// Otherwise, point to the raw code on GitHub for browser to load
|
||||||
devtoolModuleFilenameTemplate:
|
...Object.fromEntries(
|
||||||
!isTestBuild && isProdBuild
|
["", "Fallback"].map((v) => [
|
||||||
? (info) => {
|
`devtool${v}ModuleFilenameTemplate`,
|
||||||
const sourcePath = info.resourcePath.replace(/^\.\//, "");
|
!isTestBuild && isProdBuild
|
||||||
if (
|
? (info) => {
|
||||||
sourcePath.startsWith("node_modules") ||
|
if (
|
||||||
sourcePath.startsWith("webpack")
|
!path.isAbsolute(info.absoluteResourcePath) ||
|
||||||
) {
|
!existsSync(info.resourcePath) ||
|
||||||
return `no-source/${sourcePath}`;
|
info.resourcePath.startsWith("./node_modules")
|
||||||
|
) {
|
||||||
|
// Source URLs are unknown for dependencies, so we use a relative URL with a
|
||||||
|
// non - existent top directory. This results in a clean source tree in browser
|
||||||
|
// dev tools, and they stay happy getting 404s with valid requests.
|
||||||
|
return `/unknown${path.resolve("/", info.resourcePath)}`;
|
||||||
|
}
|
||||||
|
return new URL(info.resourcePath, bundle.sourceMapURL()).href;
|
||||||
}
|
}
|
||||||
return `${bundle.sourceMapURL()}/${sourcePath}`;
|
: undefined,
|
||||||
}
|
])
|
||||||
: undefined,
|
),
|
||||||
},
|
},
|
||||||
experiments: {
|
experiments: {
|
||||||
outputModule: true,
|
outputModule: true,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
|
||||||
import { html, css, LitElement } from "lit";
|
import { html, css, LitElement } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||||
@ -7,6 +6,7 @@ import "../../../src/components/ha-switch";
|
|||||||
import { HomeAssistant } from "../../../src/types";
|
import { HomeAssistant } from "../../../src/types";
|
||||||
import "./demo-card";
|
import "./demo-card";
|
||||||
import type { DemoCardConfig } from "./demo-card";
|
import type { DemoCardConfig } from "./demo-card";
|
||||||
|
import "../ha-demo-options";
|
||||||
|
|
||||||
@customElement("demo-cards")
|
@customElement("demo-cards")
|
||||||
class DemoCards extends LitElement {
|
class DemoCards extends LitElement {
|
||||||
@ -20,20 +20,14 @@ class DemoCards extends LitElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<app-toolbar>
|
<ha-demo-options>
|
||||||
<div class="filters">
|
<ha-formfield label="Show config">
|
||||||
<ha-formfield label="Show config">
|
<ha-switch @change=${this._showConfigToggled}> </ha-switch>
|
||||||
<ha-switch
|
</ha-formfield>
|
||||||
.checked=${this._showConfig}
|
<ha-formfield label="Dark theme">
|
||||||
@change=${this._showConfigToggled}
|
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
|
||||||
>
|
</ha-formfield>
|
||||||
</ha-switch>
|
</ha-demo-options>
|
||||||
</ha-formfield>
|
|
||||||
<ha-formfield label="Dark theme">
|
|
||||||
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
|
|
||||||
</ha-formfield>
|
|
||||||
</div>
|
|
||||||
</app-toolbar>
|
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<div class="cards">
|
<div class="cards">
|
||||||
${this.configs.map(
|
${this.configs.map(
|
||||||
@ -69,12 +63,6 @@ class DemoCards extends LitElement {
|
|||||||
demo-card {
|
demo-card {
|
||||||
margin: 16px 16px 32px;
|
margin: 16px 16px 32px;
|
||||||
}
|
}
|
||||||
app-toolbar {
|
|
||||||
background-color: var(--light-primary-color);
|
|
||||||
}
|
|
||||||
.filters {
|
|
||||||
margin-left: 60px;
|
|
||||||
}
|
|
||||||
ha-formfield {
|
ha-formfield {
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
|
@ -1,93 +0,0 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import "../../../src/components/ha-card";
|
|
||||||
import "../../../src/dialogs/more-info/more-info-content";
|
|
||||||
import "../../../src/state-summary/state-card-content";
|
|
||||||
|
|
||||||
class DemoMoreInfo extends PolymerElement {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
.root {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
#card {
|
|
||||||
max-width: 400px;
|
|
||||||
width: 100vw;
|
|
||||||
}
|
|
||||||
ha-card {
|
|
||||||
width: 352px;
|
|
||||||
padding: 20px 24px;
|
|
||||||
}
|
|
||||||
state-card-content {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
width: 400px;
|
|
||||||
margin: 0 16px;
|
|
||||||
overflow: auto;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 800px) {
|
|
||||||
.root {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="root">
|
|
||||||
<div id="card">
|
|
||||||
<ha-card>
|
|
||||||
<state-card-content
|
|
||||||
state-obj="[[_stateObj]]"
|
|
||||||
hass="[[hass]]"
|
|
||||||
in-dialog
|
|
||||||
></state-card-content>
|
|
||||||
|
|
||||||
<more-info-content
|
|
||||||
hass="[[hass]]"
|
|
||||||
state-obj="[[_stateObj]]"
|
|
||||||
></more-info-content>
|
|
||||||
</ha-card>
|
|
||||||
</div>
|
|
||||||
<template is="dom-if" if="[[showConfig]]">
|
|
||||||
<pre>[[_jsonEntity(_stateObj)]]</pre>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
entityId: String,
|
|
||||||
showConfig: Boolean,
|
|
||||||
_stateObj: {
|
|
||||||
type: Object,
|
|
||||||
computed: "_getState(entityId, hass.states)",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_getState(entityId, states) {
|
|
||||||
return states[entityId];
|
|
||||||
}
|
|
||||||
|
|
||||||
_jsonEntity(stateObj) {
|
|
||||||
// We are caching some things on stateObj
|
|
||||||
// (it sucks, we will remove in the future)
|
|
||||||
const tmp = {};
|
|
||||||
Object.keys(stateObj).forEach((key) => {
|
|
||||||
if (key[0] !== "_") {
|
|
||||||
tmp[key] = stateObj[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return JSON.stringify(tmp, null, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("demo-more-info", DemoMoreInfo);
|
|
93
gallery/src/components/demo-more-info.ts
Normal file
93
gallery/src/components/demo-more-info.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { LitElement, css, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import "../../../src/components/ha-card";
|
||||||
|
import "../../../src/dialogs/more-info/more-info-content";
|
||||||
|
import "../../../src/state-summary/state-card-content";
|
||||||
|
import "../ha-demo-options";
|
||||||
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
|
||||||
|
@customElement("demo-more-info")
|
||||||
|
class DemoMoreInfo extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public entityId!: string;
|
||||||
|
|
||||||
|
@property() public showConfig!: boolean;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const state = this._getState(this.entityId, this.hass.states);
|
||||||
|
return html`
|
||||||
|
<div class="root">
|
||||||
|
<div id="card">
|
||||||
|
<ha-card>
|
||||||
|
<state-card-content
|
||||||
|
.stateObj=${state}
|
||||||
|
.hass=${this.hass}
|
||||||
|
in-dialog
|
||||||
|
></state-card-content>
|
||||||
|
|
||||||
|
<more-info-content
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${state}
|
||||||
|
></more-info-content>
|
||||||
|
</ha-card>
|
||||||
|
</div>
|
||||||
|
${this.showConfig ? html`<pre>${this._jsonEntity(state)}</pre>` : ""}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getState(entityId, states) {
|
||||||
|
return states[entityId];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _jsonEntity(stateObj) {
|
||||||
|
// We are caching some things on stateObj
|
||||||
|
// (it sucks, we will remove in the future)
|
||||||
|
const tmp = {};
|
||||||
|
Object.keys(stateObj).forEach((key) => {
|
||||||
|
if (key[0] !== "_") {
|
||||||
|
tmp[key] = stateObj[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return JSON.stringify(tmp, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
#card {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
ha-card {
|
||||||
|
width: 352px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
state-card-content {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
width: 400px;
|
||||||
|
margin: 0 16px;
|
||||||
|
overflow: auto;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 800px) {
|
||||||
|
.root {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-more-info": DemoMoreInfo;
|
||||||
|
}
|
||||||
|
}
|
@ -1,83 +0,0 @@
|
|||||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
|
||||||
import "../../../src/components/ha-formfield";
|
|
||||||
import "../../../src/components/ha-switch";
|
|
||||||
import "./demo-more-info";
|
|
||||||
|
|
||||||
class DemoMoreInfos extends PolymerElement {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
#container {
|
|
||||||
min-height: calc(100vh - 128px);
|
|
||||||
background: var(--primary-background-color);
|
|
||||||
}
|
|
||||||
.cards {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
demo-more-info {
|
|
||||||
margin: 16px 16px 32px;
|
|
||||||
}
|
|
||||||
app-toolbar {
|
|
||||||
background-color: var(--light-primary-color);
|
|
||||||
}
|
|
||||||
.filters {
|
|
||||||
margin-left: 60px;
|
|
||||||
}
|
|
||||||
ha-formfield {
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<app-toolbar>
|
|
||||||
<div class="filters">
|
|
||||||
<ha-formfield label="Show entities">
|
|
||||||
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
|
|
||||||
</ha-switch>
|
|
||||||
</ha-formfield>
|
|
||||||
<ha-formfield label="Dark theme">
|
|
||||||
<ha-switch on-change="_darkThemeToggled"> </ha-switch>
|
|
||||||
</ha-formfield>
|
|
||||||
</div>
|
|
||||||
</app-toolbar>
|
|
||||||
<div id="container">
|
|
||||||
<div class="cards">
|
|
||||||
<template is="dom-repeat" items="[[entities]]">
|
|
||||||
<demo-more-info
|
|
||||||
entity-id="[[item]]"
|
|
||||||
show-config="[[_showConfig]]"
|
|
||||||
hass="[[hass]]"
|
|
||||||
></demo-more-info>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
entities: Array,
|
|
||||||
hass: Object,
|
|
||||||
_showConfig: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_showConfigToggled(ev) {
|
|
||||||
this._showConfig = ev.target.checked;
|
|
||||||
}
|
|
||||||
|
|
||||||
_darkThemeToggled(ev) {
|
|
||||||
applyThemesOnElement(this.$.container, { themes: {} }, "default", {
|
|
||||||
dark: ev.target.checked,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("demo-more-infos", DemoMoreInfos);
|
|
87
gallery/src/components/demo-more-infos.ts
Normal file
87
gallery/src/components/demo-more-infos.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { LitElement, css, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||||
|
import "../../../src/components/ha-formfield";
|
||||||
|
import "../../../src/components/ha-switch";
|
||||||
|
import "./demo-more-info";
|
||||||
|
import "../ha-demo-options";
|
||||||
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
|
||||||
|
@customElement("demo-more-infos")
|
||||||
|
class DemoMoreInfos extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public entities!: [];
|
||||||
|
|
||||||
|
@property({ attribute: false }) _showConfig: boolean = false;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<ha-demo-options>
|
||||||
|
<ha-formfield label="Show config">
|
||||||
|
<ha-switch @change=${this._showConfigToggled}> </ha-switch>
|
||||||
|
</ha-formfield>
|
||||||
|
<ha-formfield label="Dark theme">
|
||||||
|
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
|
||||||
|
</ha-formfield>
|
||||||
|
</ha-demo-options>
|
||||||
|
<div id="container">
|
||||||
|
<div class="cards">
|
||||||
|
${this.entities.map(
|
||||||
|
(item) =>
|
||||||
|
html`<demo-more-info
|
||||||
|
.entityId=${item}
|
||||||
|
.showConfig=${this._showConfig}
|
||||||
|
.hass=${this.hass}
|
||||||
|
></demo-more-info>`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
#container {
|
||||||
|
min-height: calc(100vh - 128px);
|
||||||
|
background: var(--primary-background-color);
|
||||||
|
}
|
||||||
|
.cards {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
demo-more-info {
|
||||||
|
margin: 16px 16px 32px;
|
||||||
|
}
|
||||||
|
ha-formfield {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
_showConfigToggled(ev) {
|
||||||
|
this._showConfig = ev.target.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
_darkThemeToggled(ev) {
|
||||||
|
applyThemesOnElement(
|
||||||
|
this.shadowRoot!.querySelector("#container"),
|
||||||
|
{
|
||||||
|
default_theme: "default",
|
||||||
|
default_dark_theme: "default",
|
||||||
|
themes: {},
|
||||||
|
darkMode: false,
|
||||||
|
theme: "default",
|
||||||
|
},
|
||||||
|
"default",
|
||||||
|
{
|
||||||
|
dark: ev.target.checked,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-more-infos": DemoMoreInfos;
|
||||||
|
}
|
||||||
|
}
|
47
gallery/src/ha-demo-options.ts
Normal file
47
gallery/src/ha-demo-options.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import "@material/mwc-drawer";
|
||||||
|
import "@material/mwc-top-app-bar-fixed";
|
||||||
|
import { html, css, LitElement } from "lit";
|
||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
import "../../src/components/ha-icon-button";
|
||||||
|
import "../../src/managers/notification-manager";
|
||||||
|
import { haStyle } from "../../src/resources/styles";
|
||||||
|
import "./components/page-description";
|
||||||
|
|
||||||
|
@customElement("ha-demo-options")
|
||||||
|
class HaDemoOptions extends LitElement {
|
||||||
|
render() {
|
||||||
|
return html`<slot></slot>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
background-color: var(--light-primary-color);
|
||||||
|
margin-left: 60px
|
||||||
|
margin-right: 60px;
|
||||||
|
display: var(--layout-horizontal_-_display);
|
||||||
|
-ms-flex-direction: var(--layout-horizontal_-_-ms-flex-direction);
|
||||||
|
-webkit-flex-direction: var(
|
||||||
|
--layout-horizontal_-_-webkit-flex-direction
|
||||||
|
);
|
||||||
|
flex-direction: var(--layout-horizontal_-_flex-direction);
|
||||||
|
-ms-flex-align: var(--layout-center_-_-ms-flex-align);
|
||||||
|
-webkit-align-items: var(--layout-center_-_-webkit-align-items);
|
||||||
|
align-items: var(--layout-center_-_align-items);
|
||||||
|
position: relative;
|
||||||
|
height: 64px;
|
||||||
|
padding: 0 16px;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-demo-options": HaDemoOptions;
|
||||||
|
}
|
||||||
|
}
|
@ -150,17 +150,13 @@ export class DemoHaCircularSlider extends LitElement {
|
|||||||
}
|
}
|
||||||
ha-control-circular-slider {
|
ha-control-circular-slider {
|
||||||
--control-circular-slider-color: #ff9800;
|
--control-circular-slider-color: #ff9800;
|
||||||
--control-circular-slider-background: #ff9800;
|
|
||||||
--control-circular-slider-background-opacity: 0.3;
|
|
||||||
}
|
}
|
||||||
ha-control-circular-slider[inverted] {
|
ha-control-circular-slider[inverted] {
|
||||||
--control-circular-slider-color: #2196f3;
|
--control-circular-slider-color: #2196f3;
|
||||||
--control-circular-slider-background: #2196f3;
|
|
||||||
}
|
}
|
||||||
ha-control-circular-slider[dual] {
|
ha-control-circular-slider[dual] {
|
||||||
--control-circular-slider-high-color: #2196f3;
|
--control-circular-slider-high-color: #2196f3;
|
||||||
--control-circular-slider-low-color: #ff9800;
|
--control-circular-slider-low-color: #ff9800;
|
||||||
--control-circular-slider-background: var(--disabled-color);
|
|
||||||
}
|
}
|
||||||
.field {
|
.field {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: Control Number Buttons
|
||||||
|
---
|
100
gallery/src/pages/components/ha-control-number-buttons.ts
Normal file
100
gallery/src/pages/components/ha-control-number-buttons.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { LitElement, TemplateResult, css, html } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-control-number-buttons";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
|
||||||
|
const buttons: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
class?: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
id: "basic",
|
||||||
|
label: "Basic",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "min_max_step",
|
||||||
|
label: "With min/max and step",
|
||||||
|
min: 5,
|
||||||
|
max: 25,
|
||||||
|
step: 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "custom",
|
||||||
|
label: "Custom",
|
||||||
|
class: "custom",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@customElement("demo-components-ha-control-number-buttons")
|
||||||
|
export class DemoHarControlNumberButtons extends LitElement {
|
||||||
|
@state() value = 5;
|
||||||
|
|
||||||
|
private _valueChanged(ev) {
|
||||||
|
this.value = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
${repeat(buttons, (button) => {
|
||||||
|
const { id, label, ...config } = button;
|
||||||
|
return html`
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<label id=${id}>${label}</label>
|
||||||
|
<pre>Config: ${JSON.stringify(config)}</pre>
|
||||||
|
<ha-control-number-buttons
|
||||||
|
.value=${this.value}
|
||||||
|
.min=${config.min}
|
||||||
|
.max=${config.max}
|
||||||
|
.step=${config.step}
|
||||||
|
class=${ifDefined(config.class)}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
.label=${label}
|
||||||
|
>
|
||||||
|
</ha-control-number-buttons>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-card {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.custom {
|
||||||
|
color: #2196f3;
|
||||||
|
--control-number-buttons-color: #2196f3;
|
||||||
|
--control-number-buttons-background-color: #2196f3;
|
||||||
|
--control-number-buttons-background-opacity: 0.1;
|
||||||
|
--control-number-buttons-thickness: 100px;
|
||||||
|
--control-number-buttons-border-radius: 24px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-components-ha-control-number-buttons": DemoHarControlNumberButtons;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: Control Select Menu
|
||||||
|
---
|
146
gallery/src/pages/components/ha-control-select-menu.ts
Normal file
146
gallery/src/pages/components/ha-control-select-menu.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { mdiFan, mdiFanSpeed1, mdiFanSpeed2, mdiFanSpeed3 } from "@mdi/js";
|
||||||
|
import { LitElement, TemplateResult, css, html, nothing } from "lit";
|
||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-control-select-menu";
|
||||||
|
import "../../../../src/components/ha-list-item";
|
||||||
|
import "../../../../src/components/ha-svg-icon";
|
||||||
|
|
||||||
|
type SelectMenuOptions = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
icon?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SelectMenu = {
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
class?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
options: SelectMenuOptions[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const selects: SelectMenu[] = [
|
||||||
|
{
|
||||||
|
label: "Basic select",
|
||||||
|
icon: mdiFan,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "low",
|
||||||
|
label: "Low",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "medium",
|
||||||
|
label: "Medium",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "high",
|
||||||
|
label: "High",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Select with icons",
|
||||||
|
icon: mdiFan,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "low",
|
||||||
|
label: "Low",
|
||||||
|
icon: mdiFanSpeed1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "medium",
|
||||||
|
label: "Medium",
|
||||||
|
icon: mdiFanSpeed2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "high",
|
||||||
|
label: "High",
|
||||||
|
icon: mdiFanSpeed3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Disabled select",
|
||||||
|
icon: mdiFan,
|
||||||
|
options: [],
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@customElement("demo-components-ha-control-select-menu")
|
||||||
|
export class DemoHaControlSelectMenu extends LitElement {
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-card>
|
||||||
|
${repeat(
|
||||||
|
selects,
|
||||||
|
(select) => html`
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-control-select-menu
|
||||||
|
.label=${select.label}
|
||||||
|
?disabled=${select.disabled}
|
||||||
|
fixedMenuPosition
|
||||||
|
naturalMenuWidth
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="icon" .path=${select.icon}></ha-svg-icon>
|
||||||
|
${select.options.map(
|
||||||
|
(option) => html`
|
||||||
|
<ha-list-item
|
||||||
|
.value=${option.value}
|
||||||
|
.graphic=${option.icon ? "icon" : undefined}
|
||||||
|
>
|
||||||
|
${option.icon
|
||||||
|
? html`
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="graphic"
|
||||||
|
.path=${option.icon}
|
||||||
|
></ha-svg-icon>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${option.label ?? option.value}
|
||||||
|
</ha-list-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-control-select-menu>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-card {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.custom {
|
||||||
|
--control-button-icon-color: var(--primary-color);
|
||||||
|
--control-button-background-color: var(--primary-color);
|
||||||
|
--control-button-background-opacity: 0.2;
|
||||||
|
--control-button-border-radius: 18px;
|
||||||
|
height: 100px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-components-ha-control-select-menu": DemoHaControlSelectMenu;
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@ We want to make it as easy for designers to contribute as it is for developers.
|
|||||||
|
|
||||||
- Meet us at <a href="https://discord.gg/BPBc8rZ9" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
|
- Meet us at <a href="https://discord.gg/BPBc8rZ9" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
|
||||||
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||||
- Find the lates UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||||
|
|
||||||
## Developers
|
## Developers
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ const ENTITIES = [
|
|||||||
}),
|
}),
|
||||||
getEntity("light", "bed_light", "on", {
|
getEntity("light", "bed_light", "on", {
|
||||||
friendly_name: "Bed Light",
|
friendly_name: "Bed Light",
|
||||||
supported_color_modes: [LightColorMode.HS],
|
supported_color_modes: [LightColorMode.HS, LightColorMode.COLOR_TEMP],
|
||||||
}),
|
}),
|
||||||
getEntity("light", "unavailable", "unavailable", {
|
getEntity("light", "unavailable", "unavailable", {
|
||||||
friendly_name: "Unavailable entity",
|
friendly_name: "Unavailable entity",
|
||||||
@ -116,6 +116,15 @@ const CONFIGS = [
|
|||||||
- type: "light-brightness"
|
- type: "light-brightness"
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
heading: "Light color temperature feature",
|
||||||
|
config: `
|
||||||
|
- type: tile
|
||||||
|
entity: light.bed_light
|
||||||
|
features:
|
||||||
|
- type: "color-temp"
|
||||||
|
`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
heading: "Vacuum commands feature",
|
heading: "Vacuum commands feature",
|
||||||
config: `
|
config: `
|
||||||
|
@ -284,6 +284,13 @@ const ENTITIES: HassEntity[] = [
|
|||||||
installed_version: "1.0.0",
|
installed_version: "1.0.0",
|
||||||
latest_version: "2.0.0",
|
latest_version: "2.0.0",
|
||||||
}),
|
}),
|
||||||
|
createEntity("water_heater.off", "off"),
|
||||||
|
createEntity("water_heater.eco", "eco"),
|
||||||
|
createEntity("water_heater.electric", "electric"),
|
||||||
|
createEntity("water_heater.performance", "performance"),
|
||||||
|
createEntity("water_heater.high_demand", "high_demand"),
|
||||||
|
createEntity("water_heater.heat_pump", "heat_pump"),
|
||||||
|
createEntity("water_heater.gas", "gas"),
|
||||||
];
|
];
|
||||||
|
|
||||||
function createEntity(
|
function createEntity(
|
||||||
|
3
gallery/src/pages/more-info/climate.markdown
Normal file
3
gallery/src/pages/more-info/climate.markdown
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: Climate
|
||||||
|
---
|
105
gallery/src/pages/more-info/climate.ts
Normal file
105
gallery/src/pages/more-info/climate.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, query } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/dialogs/more-info/more-info-content";
|
||||||
|
import { getEntity } from "../../../../src/fake_data/entity";
|
||||||
|
import {
|
||||||
|
MockHomeAssistant,
|
||||||
|
provideHass,
|
||||||
|
} from "../../../../src/fake_data/provide_hass";
|
||||||
|
import "../../components/demo-more-infos";
|
||||||
|
import { ClimateEntityFeature } from "../../../../src/data/climate";
|
||||||
|
|
||||||
|
const ENTITIES = [
|
||||||
|
getEntity("climate", "thermostat", "heat", {
|
||||||
|
friendly_name: "Basic heater",
|
||||||
|
hvac_modes: ["heat", "off"],
|
||||||
|
hvac_mode: "heat",
|
||||||
|
current_temperature: 18,
|
||||||
|
temperature: 20,
|
||||||
|
min_temp: 10,
|
||||||
|
max_temp: 30,
|
||||||
|
supported_features: ClimateEntityFeature.TARGET_TEMPERATURE,
|
||||||
|
}),
|
||||||
|
getEntity("climate", "ac", "cool", {
|
||||||
|
friendly_name: "Basic air conditioning",
|
||||||
|
hvac_modes: ["cool", "off"],
|
||||||
|
hvac_mode: "cool",
|
||||||
|
current_temperature: 18,
|
||||||
|
temperature: 20,
|
||||||
|
min_temp: 10,
|
||||||
|
max_temp: 30,
|
||||||
|
supported_features: ClimateEntityFeature.TARGET_TEMPERATURE,
|
||||||
|
}),
|
||||||
|
getEntity("climate", "hvac", "auto", {
|
||||||
|
friendly_name: "Basic hvac",
|
||||||
|
hvac_modes: ["auto", "off"],
|
||||||
|
hvac_mode: "auto",
|
||||||
|
current_temperature: 18,
|
||||||
|
min_temp: 10,
|
||||||
|
max_temp: 30,
|
||||||
|
target_temp_step: 1,
|
||||||
|
supported_features: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
||||||
|
target_temp_low: 20,
|
||||||
|
target_temp_high: 25,
|
||||||
|
}),
|
||||||
|
getEntity("climate", "advanced", "auto", {
|
||||||
|
friendly_name: "Advanced hvac",
|
||||||
|
supported_features:
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE |
|
||||||
|
ClimateEntityFeature.TARGET_HUMIDITY |
|
||||||
|
ClimateEntityFeature.PRESET_MODE,
|
||||||
|
hvac_modes: ["auto", "off"],
|
||||||
|
hvac_mode: "auto",
|
||||||
|
preset_modes: ["eco", "comfort", "boost"],
|
||||||
|
preset_mode: "eco",
|
||||||
|
current_temperature: 18,
|
||||||
|
min_temp: 10,
|
||||||
|
max_temp: 30,
|
||||||
|
target_temp_step: 1,
|
||||||
|
target_temp_low: 20,
|
||||||
|
target_temp_high: 25,
|
||||||
|
current_humidity: 40,
|
||||||
|
min_humidity: 0,
|
||||||
|
max_humidity: 100,
|
||||||
|
humidity: 50,
|
||||||
|
}),
|
||||||
|
getEntity("climate", "unavailable", "unavailable", {
|
||||||
|
friendly_name: "Unavailable heater",
|
||||||
|
hvac_modes: ["heat", "off"],
|
||||||
|
hvac_mode: "heat",
|
||||||
|
min_temp: 10,
|
||||||
|
max_temp: 30,
|
||||||
|
supported_features: ClimateEntityFeature.TARGET_TEMPERATURE,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
@customElement("demo-more-info-climate")
|
||||||
|
class DemoMoreInfoClimate extends LitElement {
|
||||||
|
@property() public hass!: MockHomeAssistant;
|
||||||
|
|
||||||
|
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<demo-more-infos
|
||||||
|
.hass=${this.hass}
|
||||||
|
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||||
|
></demo-more-infos>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProperties: PropertyValues) {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
const hass = provideHass(this._demoRoot);
|
||||||
|
hass.updateTranslations(null, "en");
|
||||||
|
hass.addEntities(ENTITIES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-more-info-climate": DemoMoreInfoClimate;
|
||||||
|
}
|
||||||
|
}
|
3
gallery/src/pages/more-info/humidifier.markdown
Normal file
3
gallery/src/pages/more-info/humidifier.markdown
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: Humidifier
|
||||||
|
---
|
57
gallery/src/pages/more-info/humidifier.ts
Normal file
57
gallery/src/pages/more-info/humidifier.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, query } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/dialogs/more-info/more-info-content";
|
||||||
|
import { getEntity } from "../../../../src/fake_data/entity";
|
||||||
|
import {
|
||||||
|
MockHomeAssistant,
|
||||||
|
provideHass,
|
||||||
|
} from "../../../../src/fake_data/provide_hass";
|
||||||
|
import "../../components/demo-more-infos";
|
||||||
|
|
||||||
|
const ENTITIES = [
|
||||||
|
getEntity("humidifier", "humidifier", "on", {
|
||||||
|
friendly_name: "Humidifier",
|
||||||
|
device_class: "humidifier",
|
||||||
|
current_humidity: 50,
|
||||||
|
humidity: 30,
|
||||||
|
}),
|
||||||
|
getEntity("humidifier", "dehumidifier", "on", {
|
||||||
|
friendly_name: "Dehumidifier",
|
||||||
|
device_class: "dehumidifier",
|
||||||
|
current_humidity: 50,
|
||||||
|
humidity: 30,
|
||||||
|
}),
|
||||||
|
getEntity("humidifier", "unavailable", "unavailable", {
|
||||||
|
friendly_name: "Unavailable humidifier",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
@customElement("demo-more-info-humidifier")
|
||||||
|
class DemoMoreInfoHumidifier extends LitElement {
|
||||||
|
@property() public hass!: MockHomeAssistant;
|
||||||
|
|
||||||
|
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<demo-more-infos
|
||||||
|
.hass=${this.hass}
|
||||||
|
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||||
|
></demo-more-infos>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProperties: PropertyValues) {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
const hass = provideHass(this._demoRoot);
|
||||||
|
hass.updateTranslations(null, "en");
|
||||||
|
hass.addEntities(ENTITIES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-more-info-humidifier": DemoMoreInfoHumidifier;
|
||||||
|
}
|
||||||
|
}
|
3
gallery/src/pages/more-info/water-heater.markdown
Normal file
3
gallery/src/pages/more-info/water-heater.markdown
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: Water Heater
|
||||||
|
---
|
70
gallery/src/pages/more-info/water-heater.ts
Normal file
70
gallery/src/pages/more-info/water-heater.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, query } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import { WaterHeaterEntityFeature } from "../../../../src/data/water_heater";
|
||||||
|
import "../../../../src/dialogs/more-info/more-info-content";
|
||||||
|
import { getEntity } from "../../../../src/fake_data/entity";
|
||||||
|
import {
|
||||||
|
MockHomeAssistant,
|
||||||
|
provideHass,
|
||||||
|
} from "../../../../src/fake_data/provide_hass";
|
||||||
|
import "../../components/demo-more-infos";
|
||||||
|
|
||||||
|
const ENTITIES = [
|
||||||
|
getEntity("water_heater", "basic", "eco", {
|
||||||
|
friendly_name: "Basic heater",
|
||||||
|
operation_list: ["heat_pump", "eco", "performance", "off"],
|
||||||
|
operation_mode: "eco",
|
||||||
|
away_mode: "off",
|
||||||
|
target_temp_step: 1,
|
||||||
|
current_temperature: 55,
|
||||||
|
temperature: 60,
|
||||||
|
min_temp: 20,
|
||||||
|
max_temp: 70,
|
||||||
|
supported_features:
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
WaterHeaterEntityFeature.TARGET_TEMPERATURE |
|
||||||
|
WaterHeaterEntityFeature.OPERATION_MODE |
|
||||||
|
WaterHeaterEntityFeature.AWAY_MODE,
|
||||||
|
}),
|
||||||
|
getEntity("water_heater", "unavailable", "unavailable", {
|
||||||
|
friendly_name: "Unavailable heater",
|
||||||
|
operation_list: ["heat_pump", "eco", "performance", "off"],
|
||||||
|
operation_mode: "off",
|
||||||
|
min_temp: 20,
|
||||||
|
max_temp: 70,
|
||||||
|
supported_features:
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
WaterHeaterEntityFeature.TARGET_TEMPERATURE |
|
||||||
|
WaterHeaterEntityFeature.OPERATION_MODE,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
@customElement("demo-more-info-water-heater")
|
||||||
|
class DemoMoreInfoWaterHeater extends LitElement {
|
||||||
|
@property() public hass!: MockHomeAssistant;
|
||||||
|
|
||||||
|
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<demo-more-infos
|
||||||
|
.hass=${this.hass}
|
||||||
|
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||||
|
></demo-more-infos>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProperties: PropertyValues) {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
const hass = provideHass(this._demoRoot);
|
||||||
|
hass.updateTranslations(null, "en");
|
||||||
|
hass.addEntities(ENTITIES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-more-info-water-heater": DemoMoreInfoWaterHeater;
|
||||||
|
}
|
||||||
|
}
|
@ -31,6 +31,7 @@ export class HassioUploadBackup extends LitElement {
|
|||||||
.icon=${mdiFolderUpload}
|
.icon=${mdiFolderUpload}
|
||||||
accept="application/x-tar"
|
accept="application/x-tar"
|
||||||
label="Upload backup"
|
label="Upload backup"
|
||||||
|
supports="Supports .TAR files"
|
||||||
@file-picked=${this._uploadFile}
|
@file-picked=${this._uploadFile}
|
||||||
auto-open-file-dialog
|
auto-open-file-dialog
|
||||||
></ha-file-upload>
|
></ha-file-upload>
|
||||||
|
@ -173,6 +173,7 @@ class HassioBackupDialog
|
|||||||
private async _restoreClicked() {
|
private async _restoreClicked() {
|
||||||
const backupDetails = this._backupContent.backupDetails();
|
const backupDetails = this._backupContent.backupDetails();
|
||||||
this._restoringBackup = true;
|
this._restoringBackup = true;
|
||||||
|
this._dialogParams?.onRestoring?.();
|
||||||
if (this._backupContent.backupType === "full") {
|
if (this._backupContent.backupType === "full") {
|
||||||
await this._fullRestoreClicked(backupDetails);
|
await this._fullRestoreClicked(backupDetails);
|
||||||
} else {
|
} else {
|
||||||
@ -219,7 +220,7 @@ class HassioBackupDialog
|
|||||||
this._error = error.body.message;
|
this._error = error.body.message;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fireEvent(this, "restoring");
|
this._dialogParams?.onRestoring?.();
|
||||||
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
|
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(backupDetails),
|
body: JSON.stringify(backupDetails),
|
||||||
@ -268,7 +269,7 @@ class HassioBackupDialog
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
fireEvent(this, "restoring");
|
this._dialogParams?.onRestoring?.();
|
||||||
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
|
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(backupDetails),
|
body: JSON.stringify(backupDetails),
|
||||||
|
@ -5,6 +5,7 @@ import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
|||||||
export interface HassioBackupDialogParams {
|
export interface HassioBackupDialogParams {
|
||||||
slug: string;
|
slug: string;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
|
onRestoring?: () => void;
|
||||||
onboarding?: boolean;
|
onboarding?: boolean;
|
||||||
supervisor?: Supervisor;
|
supervisor?: Supervisor;
|
||||||
localize?: LocalizeFunc;
|
localize?: LocalizeFunc;
|
||||||
|
91
package.json
91
package.json
@ -25,15 +25,15 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.22.6",
|
"@babel/runtime": "7.22.11",
|
||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.4",
|
||||||
"@codemirror/autocomplete": "6.9.0",
|
"@codemirror/autocomplete": "6.9.0",
|
||||||
"@codemirror/commands": "6.2.4",
|
"@codemirror/commands": "6.2.5",
|
||||||
"@codemirror/language": "6.8.0",
|
"@codemirror/language": "6.9.0",
|
||||||
"@codemirror/legacy-modes": "6.3.3",
|
"@codemirror/legacy-modes": "6.3.3",
|
||||||
"@codemirror/search": "6.5.0",
|
"@codemirror/search": "6.5.2",
|
||||||
"@codemirror/state": "6.2.1",
|
"@codemirror/state": "6.2.1",
|
||||||
"@codemirror/view": "6.15.3",
|
"@codemirror/view": "6.16.0",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.10.0",
|
"@formatjs/intl-datetimeformat": "6.10.0",
|
||||||
"@formatjs/intl-displaynames": "6.5.0",
|
"@formatjs/intl-displaynames": "6.5.0",
|
||||||
@ -50,10 +50,10 @@
|
|||||||
"@fullcalendar/luxon3": "6.1.8",
|
"@fullcalendar/luxon3": "6.1.8",
|
||||||
"@fullcalendar/timegrid": "6.1.8",
|
"@fullcalendar/timegrid": "6.1.8",
|
||||||
"@lezer/highlight": "1.1.6",
|
"@lezer/highlight": "1.1.6",
|
||||||
"@lit-labs/context": "0.3.3",
|
"@lit-labs/context": "0.4.0",
|
||||||
"@lit-labs/motion": "1.0.3",
|
"@lit-labs/motion": "1.0.4",
|
||||||
"@lit-labs/virtualizer": "2.0.4",
|
"@lit-labs/virtualizer": "2.0.6",
|
||||||
"@lrnwebcomponents/simple-tooltip": "7.0.11",
|
"@lrnwebcomponents/simple-tooltip": "7.0.16",
|
||||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/mwc-button": "0.27.0",
|
"@material/mwc-button": "0.27.0",
|
||||||
@ -79,7 +79,7 @@
|
|||||||
"@material/mwc-top-app-bar": "0.27.0",
|
"@material/mwc-top-app-bar": "0.27.0",
|
||||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/web": "=1.0.0-pre.13",
|
"@material/web": "=1.0.0-pre.16",
|
||||||
"@mdi/js": "7.2.96",
|
"@mdi/js": "7.2.96",
|
||||||
"@mdi/svg": "7.2.96",
|
"@mdi/svg": "7.2.96",
|
||||||
"@polymer/app-layout": "3.1.0",
|
"@polymer/app-layout": "3.1.0",
|
||||||
@ -94,8 +94,8 @@
|
|||||||
"@polymer/paper-toast": "3.0.1",
|
"@polymer/paper-toast": "3.0.1",
|
||||||
"@polymer/polymer": "3.5.1",
|
"@polymer/polymer": "3.5.1",
|
||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@vaadin/combo-box": "24.1.4",
|
"@vaadin/combo-box": "24.1.6",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.1.4",
|
"@vaadin/vaadin-themable-mixin": "24.1.6",
|
||||||
"@vibrant/color": "3.2.1-alpha.1",
|
"@vibrant/color": "3.2.1-alpha.1",
|
||||||
"@vibrant/core": "3.2.1-alpha.1",
|
"@vibrant/core": "3.2.1-alpha.1",
|
||||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||||
@ -103,10 +103,10 @@
|
|||||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||||
"app-datepicker": "5.1.1",
|
"app-datepicker": "5.1.1",
|
||||||
"chart.js": "3.3.2",
|
"chart.js": "4.3.3",
|
||||||
"comlink": "4.4.1",
|
"comlink": "4.4.1",
|
||||||
"core-js": "3.31.1",
|
"core-js": "3.32.1",
|
||||||
"cropperjs": "1.5.13",
|
"cropperjs": "1.6.0",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"date-fns-tz": "2.0.0",
|
"date-fns-tz": "2.0.0",
|
||||||
"deep-clone-simple": "1.1.1",
|
"deep-clone-simple": "1.1.1",
|
||||||
@ -120,9 +120,9 @@
|
|||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-draw": "1.0.4",
|
"leaflet-draw": "1.0.4",
|
||||||
"lit": "2.7.6",
|
"lit": "2.8.0",
|
||||||
"luxon": "3.3.0",
|
"luxon": "3.4.2",
|
||||||
"marked": "4.3.0",
|
"marked": "7.0.5",
|
||||||
"memoize-one": "6.0.0",
|
"memoize-one": "6.0.0",
|
||||||
"node-vibrant": "3.2.1-alpha.1",
|
"node-vibrant": "3.2.1-alpha.1",
|
||||||
"proxy-polyfill": "0.3.2",
|
"proxy-polyfill": "0.3.2",
|
||||||
@ -133,10 +133,12 @@
|
|||||||
"roboto-fontface": "0.10.0",
|
"roboto-fontface": "0.10.0",
|
||||||
"rrule": "2.7.2",
|
"rrule": "2.7.2",
|
||||||
"sortablejs": "1.15.0",
|
"sortablejs": "1.15.0",
|
||||||
|
"stacktrace-js": "2.0.2",
|
||||||
"superstruct": "1.0.3",
|
"superstruct": "1.0.3",
|
||||||
"tinykeys": "2.1.0",
|
"tinykeys": "2.1.0",
|
||||||
"tsparticles-engine": "2.11.0",
|
"tsparticles-engine": "2.12.0",
|
||||||
"tsparticles-preset-links": "2.11.0",
|
"tsparticles-preset-links": "2.12.0",
|
||||||
|
"ua-parser-js": "1.0.35",
|
||||||
"unfetch": "5.0.0",
|
"unfetch": "5.0.0",
|
||||||
"vis-data": "7.1.6",
|
"vis-data": "7.1.6",
|
||||||
"vis-network": "9.1.6",
|
"vis-network": "9.1.6",
|
||||||
@ -152,20 +154,20 @@
|
|||||||
"xss": "1.0.14"
|
"xss": "1.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.22.9",
|
"@babel/core": "7.22.11",
|
||||||
"@babel/plugin-proposal-decorators": "7.22.7",
|
"@babel/plugin-proposal-decorators": "7.22.10",
|
||||||
"@babel/plugin-transform-runtime": "7.22.9",
|
"@babel/plugin-transform-runtime": "7.22.10",
|
||||||
"@babel/preset-env": "7.22.9",
|
"@babel/preset-env": "7.22.10",
|
||||||
"@babel/preset-typescript": "7.22.5",
|
"@babel/preset-typescript": "7.22.11",
|
||||||
"@koa/cors": "4.0.0",
|
"@koa/cors": "4.0.0",
|
||||||
"@octokit/auth-oauth-device": "6.0.0",
|
"@octokit/auth-oauth-device": "6.0.0",
|
||||||
"@octokit/plugin-retry": "6.0.0",
|
"@octokit/plugin-retry": "6.0.0",
|
||||||
"@octokit/rest": "20.0.1",
|
"@octokit/rest": "20.0.1",
|
||||||
"@open-wc/dev-server-hmr": "0.1.4",
|
"@open-wc/dev-server-hmr": "0.1.4",
|
||||||
"@rollup/plugin-babel": "6.0.3",
|
"@rollup/plugin-babel": "6.0.3",
|
||||||
"@rollup/plugin-commonjs": "25.0.3",
|
"@rollup/plugin-commonjs": "25.0.4",
|
||||||
"@rollup/plugin-json": "6.0.0",
|
"@rollup/plugin-json": "6.0.0",
|
||||||
"@rollup/plugin-node-resolve": "15.1.0",
|
"@rollup/plugin-node-resolve": "15.2.1",
|
||||||
"@rollup/plugin-replace": "5.0.2",
|
"@rollup/plugin-replace": "5.0.2",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.2",
|
"@types/babel__plugin-transform-runtime": "7.9.2",
|
||||||
"@types/chromecast-caf-receiver": "6.0.9",
|
"@types/chromecast-caf-receiver": "6.0.9",
|
||||||
@ -177,29 +179,29 @@
|
|||||||
"@types/leaflet": "1.9.3",
|
"@types/leaflet": "1.9.3",
|
||||||
"@types/leaflet-draw": "1.0.7",
|
"@types/leaflet-draw": "1.0.7",
|
||||||
"@types/luxon": "3.3.1",
|
"@types/luxon": "3.3.1",
|
||||||
"@types/marked": "4.3.1",
|
|
||||||
"@types/mocha": "10.0.1",
|
"@types/mocha": "10.0.1",
|
||||||
"@types/qrcode": "1.5.1",
|
"@types/qrcode": "1.5.1",
|
||||||
"@types/serve-handler": "6.1.1",
|
"@types/serve-handler": "6.1.1",
|
||||||
"@types/sortablejs": "1.15.1",
|
"@types/sortablejs": "1.15.1",
|
||||||
"@types/tar": "6.1.5",
|
"@types/tar": "6.1.5",
|
||||||
|
"@types/ua-parser-js": "0.7.36",
|
||||||
"@types/webspeechapi": "0.0.29",
|
"@types/webspeechapi": "0.0.29",
|
||||||
"@typescript-eslint/eslint-plugin": "6.2.0",
|
"@typescript-eslint/eslint-plugin": "6.4.1",
|
||||||
"@typescript-eslint/parser": "6.2.0",
|
"@typescript-eslint/parser": "6.4.1",
|
||||||
"@web/dev-server": "0.1.38",
|
"@web/dev-server": "0.1.38",
|
||||||
"@web/dev-server-rollup": "0.4.1",
|
"@web/dev-server-rollup": "0.4.1",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.3",
|
||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"chai": "4.3.7",
|
"chai": "4.3.8",
|
||||||
"del": "7.0.0",
|
"del": "7.0.0",
|
||||||
"eslint": "8.46.0",
|
"eslint": "8.48.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-airbnb-typescript": "17.1.0",
|
"eslint-config-airbnb-typescript": "17.1.0",
|
||||||
"eslint-config-prettier": "8.9.0",
|
"eslint-config-prettier": "9.0.0",
|
||||||
"eslint-import-resolver-webpack": "0.13.2",
|
"eslint-import-resolver-webpack": "0.13.7",
|
||||||
"eslint-plugin-disable": "2.0.3",
|
"eslint-plugin-disable": "2.0.3",
|
||||||
"eslint-plugin-import": "2.28.0",
|
"eslint-plugin-import": "2.28.1",
|
||||||
"eslint-plugin-lit": "1.8.3",
|
"eslint-plugin-lit": "1.9.1",
|
||||||
"eslint-plugin-lit-a11y": "3.0.0",
|
"eslint-plugin-lit-a11y": "3.0.0",
|
||||||
"eslint-plugin-unused-imports": "3.0.0",
|
"eslint-plugin-unused-imports": "3.0.0",
|
||||||
"eslint-plugin-wc": "1.5.0",
|
"eslint-plugin-wc": "1.5.0",
|
||||||
@ -215,19 +217,18 @@
|
|||||||
"gulp-zopfli-green": "6.0.1",
|
"gulp-zopfli-green": "6.0.1",
|
||||||
"html-minifier-terser": "7.2.0",
|
"html-minifier-terser": "7.2.0",
|
||||||
"husky": "8.0.3",
|
"husky": "8.0.3",
|
||||||
"instant-mocha": "1.5.1",
|
"instant-mocha": "1.5.2",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lint-staged": "13.2.3",
|
"lint-staged": "14.0.1",
|
||||||
"lit-analyzer": "2.0.0-pre.3",
|
"lit-analyzer": "2.0.0-pre.3",
|
||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
"magic-string": "0.30.1",
|
"magic-string": "0.30.3",
|
||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
"merge-stream": "2.0.0",
|
|
||||||
"mocha": "10.2.0",
|
"mocha": "10.2.0",
|
||||||
"object-hash": "3.0.0",
|
"object-hash": "3.0.0",
|
||||||
"open": "9.1.0",
|
"open": "9.1.0",
|
||||||
"pinst": "3.0.0",
|
"pinst": "3.0.0",
|
||||||
"prettier": "3.0.0",
|
"prettier": "3.0.2",
|
||||||
"rollup": "2.79.1",
|
"rollup": "2.79.1",
|
||||||
"rollup-plugin-string": "3.0.0",
|
"rollup-plugin-string": "3.0.0",
|
||||||
"rollup-plugin-terser": "7.0.2",
|
"rollup-plugin-terser": "7.0.2",
|
||||||
@ -235,11 +236,11 @@
|
|||||||
"serve-handler": "6.1.5",
|
"serve-handler": "6.1.5",
|
||||||
"sinon": "15.2.0",
|
"sinon": "15.2.0",
|
||||||
"source-map-url": "0.4.1",
|
"source-map-url": "0.4.1",
|
||||||
"systemjs": "6.14.1",
|
"systemjs": "6.14.2",
|
||||||
"tar": "6.1.15",
|
"tar": "6.1.15",
|
||||||
"terser-webpack-plugin": "5.3.9",
|
"terser-webpack-plugin": "5.3.9",
|
||||||
"ts-lit-plugin": "2.0.0-pre.1",
|
"ts-lit-plugin": "2.0.0-pre.1",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.2.2",
|
||||||
"vinyl-buffer": "1.0.1",
|
"vinyl-buffer": "1.0.1",
|
||||||
"vinyl-source-stream": "2.0.0",
|
"vinyl-source-stream": "2.0.0",
|
||||||
"webpack": "5.88.2",
|
"webpack": "5.88.2",
|
||||||
@ -256,5 +257,5 @@
|
|||||||
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch",
|
"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"
|
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.6.1"
|
"packageManager": "yarn@3.6.3"
|
||||||
}
|
}
|
||||||
|
BIN
public/static/images/logo_discord.png
Normal file
BIN
public/static/images/logo_discord.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
public/static/images/logo_twitter.png
Normal file
BIN
public/static/images/logo_twitter.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20230802.1"
|
version = "20230830.0"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
"lockFileMaintenance": {
|
"lockFileMaintenance": {
|
||||||
"description": ["Run after patch releases but before next beta"],
|
"description": ["Run after patch releases but before next beta"],
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"schedule": ["on the 19th day of the month"]
|
"schedule": ["on the 19th day of the month before 4am"]
|
||||||
},
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import punycode from "punycode";
|
import punycode from "punycode";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
nothing,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||||
|
import "../components/ha-alert";
|
||||||
import {
|
import {
|
||||||
AuthProvider,
|
AuthProvider,
|
||||||
AuthUrlSearchParams,
|
AuthUrlSearchParams,
|
||||||
@ -14,6 +22,11 @@ import "./ha-auth-flow";
|
|||||||
|
|
||||||
import("./ha-pick-auth-provider");
|
import("./ha-pick-auth-provider");
|
||||||
|
|
||||||
|
const appNames = {
|
||||||
|
"https://home-assistant.io/iOS": "iOS",
|
||||||
|
"https://home-assistant.io/android": "Android",
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("ha-authorize")
|
@customElement("ha-authorize")
|
||||||
export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||||
@property() public clientId?: string;
|
@property() public clientId?: string;
|
||||||
@ -22,13 +35,18 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
|
|
||||||
@property() public oauth2State?: string;
|
@property() public oauth2State?: string;
|
||||||
|
|
||||||
|
@property() public translationFragment = "page-authorize";
|
||||||
|
|
||||||
@state() private _authProvider?: AuthProvider;
|
@state() private _authProvider?: AuthProvider;
|
||||||
|
|
||||||
@state() private _authProviders?: AuthProvider[];
|
@state() private _authProviders?: AuthProvider[];
|
||||||
|
|
||||||
|
@state() private _ownInstance = false;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.translationFragment = "page-authorize";
|
|
||||||
const query = extractSearchParamsObject() as AuthUrlSearchParams;
|
const query = extractSearchParamsObject() as AuthUrlSearchParams;
|
||||||
if (query.client_id) {
|
if (query.client_id) {
|
||||||
this.clientId = query.client_id;
|
this.clientId = query.client_id;
|
||||||
@ -42,42 +60,49 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
|
if (this._error) {
|
||||||
|
return html`<ha-alert alert-type="error"
|
||||||
|
>${this._error} ${this.redirectUri}</ha-alert
|
||||||
|
>`;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this._authProviders) {
|
if (!this._authProviders) {
|
||||||
return html`
|
return html`
|
||||||
<p>${this.localize("ui.panel.page-authorize.initializing")}</p>
|
<p>${this.localize("ui.panel.page-authorize.initializing")}</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't have a good approach yet to map text markup in localization.
|
|
||||||
// So we sanitize the translation with innerText and then inject
|
|
||||||
// the name with a bold tag.
|
|
||||||
const loggingInWith = document.createElement("div");
|
|
||||||
loggingInWith.innerText = this.localize(
|
|
||||||
"ui.panel.page-authorize.logging_in_with",
|
|
||||||
"authProviderName",
|
|
||||||
"NAME"
|
|
||||||
);
|
|
||||||
loggingInWith.innerHTML = loggingInWith.innerHTML.replace(
|
|
||||||
"**NAME**",
|
|
||||||
`<b>${this._authProvider!.name}</b>`
|
|
||||||
);
|
|
||||||
|
|
||||||
const inactiveProviders = this._authProviders.filter(
|
const inactiveProviders = this._authProviders.filter(
|
||||||
(prv) => prv !== this._authProvider
|
(prv) => prv !== this._authProvider
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const app = this.clientId && this.clientId in appNames;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<p>
|
${!this._ownInstance
|
||||||
${this.localize(
|
? html`<ha-alert .alertType=${app ? "info" : "warning"}>
|
||||||
"ui.panel.page-authorize.authorizing_client",
|
${app
|
||||||
"clientId",
|
? this.localize("ui.panel.page-authorize.authorizing_app", {
|
||||||
this.clientId ? punycode.toASCII(this.clientId) : this.clientId
|
app: appNames[this.clientId!],
|
||||||
)}
|
})
|
||||||
</p>
|
: this.localize("ui.panel.page-authorize.authorizing_client", {
|
||||||
${loggingInWith}
|
clientId: html`<b
|
||||||
|
>${this.clientId
|
||||||
|
? punycode.toASCII(this.clientId)
|
||||||
|
: this.clientId}</b
|
||||||
|
>`,
|
||||||
|
})}
|
||||||
|
</ha-alert>`
|
||||||
|
: html`<p>${this.localize("ui.panel.page-authorize.authorizing")}</p>`}
|
||||||
|
${inactiveProviders.length > 0
|
||||||
|
? html`<p>
|
||||||
|
${this.localize("ui.panel.page-authorize.logging_in_with", {
|
||||||
|
authProviderName: html`<b>${this._authProvider!.name}</b>`,
|
||||||
|
})}
|
||||||
|
</p>`
|
||||||
|
: nothing}
|
||||||
|
|
||||||
<ha-auth-flow
|
<ha-auth-flow
|
||||||
.resources=${this.resources}
|
|
||||||
.clientId=${this.clientId}
|
.clientId=${this.clientId}
|
||||||
.redirectUri=${this.redirectUri}
|
.redirectUri=${this.redirectUri}
|
||||||
.oauth2State=${this.oauth2State}
|
.oauth2State=${this.oauth2State}
|
||||||
@ -100,6 +125,31 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
|
if (!this.redirectUri) {
|
||||||
|
this._error = "Invalid redirect URI";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url: URL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
url = new URL(this.redirectUri);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = "Invalid redirect URI";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
// eslint-disable-next-line no-script-url
|
||||||
|
["javascript:", "data:", "vbscript:", "file:", "about:"].includes(
|
||||||
|
url.protocol
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this._error = "Invalid redirect URI";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._fetchAuthProviders();
|
this._fetchAuthProviders();
|
||||||
|
|
||||||
if (matchMedia("(prefers-color-scheme: dark)").matches) {
|
if (matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
@ -118,15 +168,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.redirectUri) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are logging into the instance that is hosting this auth form
|
// If we are logging into the instance that is hosting this auth form
|
||||||
// we will register the service worker to start preloading.
|
// we will register the service worker to start preloading.
|
||||||
const tempA = document.createElement("a");
|
if (url.host === location.host) {
|
||||||
tempA.href = this.redirectUri!;
|
this._ownInstance = true;
|
||||||
if (tempA.host === location.host) {
|
|
||||||
registerServiceWorker(this, false);
|
registerServiceWorker(this, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,13 +201,14 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (authProviders.length === 0) {
|
if (authProviders.length === 0) {
|
||||||
alert("No auth providers returned. Unable to finish login.");
|
this._error = "No auth providers returned. Unable to finish login.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._authProviders = authProviders;
|
this._authProviders = authProviders;
|
||||||
this._authProvider = authProviders[0];
|
this._authProvider = authProviders[0];
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
this._error = "Unable to fetch auth providers.";
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
console.error("Error loading auth providers", err);
|
console.error("Error loading auth providers", err);
|
||||||
}
|
}
|
||||||
@ -182,6 +228,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
display: block;
|
display: block;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
ha-alert {
|
||||||
|
display: block;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
p {
|
p {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
@ -107,6 +107,7 @@ export class HaPasswordManagerPolyfill extends LitElement {
|
|||||||
.value=${this.stepData[schema.name] || ""}
|
.value=${this.stepData[schema.name] || ""}
|
||||||
.autocomplete=${schema.autocomplete}
|
.autocomplete=${schema.autocomplete}
|
||||||
@input=${this._valueChanged}
|
@input=${this._valueChanged}
|
||||||
|
@change=${this._valueChanged}
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { clamp } from "../number/clamp";
|
import { clamp } from "../number/clamp";
|
||||||
|
|
||||||
const DEFAULT_MIN_KELVIN = 2700;
|
export const DEFAULT_MIN_KELVIN = 2700;
|
||||||
const DEFAULT_MAX_KELVIN = 6500;
|
export const DEFAULT_MAX_KELVIN = 6500;
|
||||||
|
|
||||||
export const temperature2rgb = (
|
export const temperature2rgb = (
|
||||||
temperature: number
|
temperature: number
|
||||||
|
@ -49,6 +49,7 @@ import {
|
|||||||
mdiProgressClock,
|
mdiProgressClock,
|
||||||
mdiRayVertex,
|
mdiRayVertex,
|
||||||
mdiRemote,
|
mdiRemote,
|
||||||
|
mdiRobotMower,
|
||||||
mdiRobotVacuum,
|
mdiRobotVacuum,
|
||||||
mdiScriptText,
|
mdiScriptText,
|
||||||
mdiSineWave,
|
mdiSineWave,
|
||||||
@ -99,6 +100,7 @@ export const FIXED_DOMAIN_ICONS = {
|
|||||||
input_number: mdiRayVertex,
|
input_number: mdiRayVertex,
|
||||||
input_select: mdiFormatListBulleted,
|
input_select: mdiFormatListBulleted,
|
||||||
input_text: mdiFormTextbox,
|
input_text: mdiFormTextbox,
|
||||||
|
lawn_mower: mdiRobotMower,
|
||||||
light: mdiLightbulb,
|
light: mdiLightbulb,
|
||||||
mailbox: mdiMailbox,
|
mailbox: mdiMailbox,
|
||||||
notify: mdiCommentAlert,
|
notify: mdiCommentAlert,
|
||||||
@ -176,6 +178,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
|
|||||||
|
|
||||||
/** Domains that have a state card. */
|
/** Domains that have a state card. */
|
||||||
export const DOMAINS_WITH_CARD = [
|
export const DOMAINS_WITH_CARD = [
|
||||||
|
"alert",
|
||||||
"button",
|
"button",
|
||||||
"climate",
|
"climate",
|
||||||
"cover",
|
"cover",
|
||||||
@ -186,6 +189,7 @@ export const DOMAINS_WITH_CARD = [
|
|||||||
"input_number",
|
"input_number",
|
||||||
"input_text",
|
"input_text",
|
||||||
"humidifier",
|
"humidifier",
|
||||||
|
"lawn_mower",
|
||||||
"lock",
|
"lock",
|
||||||
"media_player",
|
"media_player",
|
||||||
"number",
|
"number",
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
import { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||||
import { html, TemplateResult } from "lit";
|
import {
|
||||||
import { until } from "lit/directives/until";
|
DOMAIN_ATTRIBUTES_UNITS,
|
||||||
|
TEMPERATURE_ATTRIBUTES,
|
||||||
|
} from "../../data/entity_attributes";
|
||||||
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||||
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
|
import { WeatherEntity, getWeatherUnit } from "../../data/weather";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import checkValidDate from "../datetime/check_valid_date";
|
import checkValidDate from "../datetime/check_valid_date";
|
||||||
import { formatDate } from "../datetime/format_date";
|
import { formatDate } from "../datetime/format_date";
|
||||||
@ -10,11 +14,10 @@ import { formatNumber } from "../number/format_number";
|
|||||||
import { capitalizeFirstLetter } from "../string/capitalize-first-letter";
|
import { capitalizeFirstLetter } from "../string/capitalize-first-letter";
|
||||||
import { isDate } from "../string/is_date";
|
import { isDate } from "../string/is_date";
|
||||||
import { isTimestamp } from "../string/is_timestamp";
|
import { isTimestamp } from "../string/is_timestamp";
|
||||||
|
import { blankBeforePercent } from "../translations/blank_before_percent";
|
||||||
import { LocalizeFunc } from "../translations/localize";
|
import { LocalizeFunc } from "../translations/localize";
|
||||||
import { computeDomain } from "./compute_domain";
|
import { computeDomain } from "./compute_domain";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { computeStateDomain } from "./compute_state_domain";
|
||||||
|
|
||||||
let jsYamlPromise: Promise<typeof import("../../resources/js-yaml-dump")>;
|
|
||||||
|
|
||||||
export const computeAttributeValueDisplay = (
|
export const computeAttributeValueDisplay = (
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
@ -24,7 +27,7 @@ export const computeAttributeValueDisplay = (
|
|||||||
entities: HomeAssistant["entities"],
|
entities: HomeAssistant["entities"],
|
||||||
attribute: string,
|
attribute: string,
|
||||||
value?: any
|
value?: any
|
||||||
): string | TemplateResult => {
|
): string => {
|
||||||
const attributeValue =
|
const attributeValue =
|
||||||
value !== undefined ? value : stateObj.attributes[attribute];
|
value !== undefined ? value : stateObj.attributes[attribute];
|
||||||
|
|
||||||
@ -35,28 +38,44 @@ export const computeAttributeValueDisplay = (
|
|||||||
|
|
||||||
// Number value, return formatted number
|
// Number value, return formatted number
|
||||||
if (typeof attributeValue === "number") {
|
if (typeof attributeValue === "number") {
|
||||||
return formatNumber(attributeValue, locale);
|
const formattedValue = formatNumber(attributeValue, locale);
|
||||||
|
|
||||||
|
const domain = computeStateDomain(stateObj);
|
||||||
|
|
||||||
|
let unit = DOMAIN_ATTRIBUTES_UNITS[domain]?.[attribute] as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (domain === "light" && attribute === "brightness") {
|
||||||
|
const percentage = Math.round((attributeValue / 255) * 100);
|
||||||
|
return `${percentage}${blankBeforePercent(locale)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain === "weather") {
|
||||||
|
unit = getWeatherUnit(config, stateObj as WeatherEntity, attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === "%") {
|
||||||
|
return `${formattedValue}${blankBeforePercent(locale)}${unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === "°") {
|
||||||
|
return `${formattedValue}${unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit) {
|
||||||
|
return `${formattedValue} ${unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TEMPERATURE_ATTRIBUTES.has(attribute)) {
|
||||||
|
return `${formattedValue} ${config.unit_system.temperature}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling in case this is a string with an known format
|
// Special handling in case this is a string with an known format
|
||||||
if (typeof attributeValue === "string") {
|
if (typeof attributeValue === "string") {
|
||||||
// URL handling
|
|
||||||
if (attributeValue.startsWith("http")) {
|
|
||||||
try {
|
|
||||||
// If invalid URL, exception will be raised
|
|
||||||
const url = new URL(attributeValue);
|
|
||||||
if (url.protocol === "http:" || url.protocol === "https:")
|
|
||||||
return html`<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href=${attributeValue}
|
|
||||||
>${attributeValue}</a
|
|
||||||
>`;
|
|
||||||
} catch (_) {
|
|
||||||
// Nothing to do here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date handling
|
// Date handling
|
||||||
if (isDate(attributeValue, true)) {
|
if (isDate(attributeValue, true)) {
|
||||||
// Timestamp handling
|
// Timestamp handling
|
||||||
@ -81,13 +100,8 @@ export const computeAttributeValueDisplay = (
|
|||||||
attributeValue.some((val) => val instanceof Object)) ||
|
attributeValue.some((val) => val instanceof Object)) ||
|
||||||
(!Array.isArray(attributeValue) && attributeValue instanceof Object)
|
(!Array.isArray(attributeValue) && attributeValue instanceof Object)
|
||||||
) {
|
) {
|
||||||
if (!jsYamlPromise) {
|
return JSON.stringify(attributeValue);
|
||||||
jsYamlPromise = import("../../resources/js-yaml-dump");
|
|
||||||
}
|
|
||||||
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.dump(attributeValue));
|
|
||||||
return html`<pre>${until(yaml, "")}</pre>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is an array, try to determine the display value for each item
|
// If this is an array, try to determine the display value for each item
|
||||||
if (Array.isArray(attributeValue)) {
|
if (Array.isArray(attributeValue)) {
|
||||||
return attributeValue
|
return attributeValue
|
||||||
|
@ -26,6 +26,7 @@ export const FIXED_DOMAIN_STATES = {
|
|||||||
humidifier: ["on", "off"],
|
humidifier: ["on", "off"],
|
||||||
input_boolean: ["on", "off"],
|
input_boolean: ["on", "off"],
|
||||||
input_button: [],
|
input_button: [],
|
||||||
|
lawn_mower: ["error", "paused", "mowing", "docked"],
|
||||||
light: ["on", "off"],
|
light: ["on", "off"],
|
||||||
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
|
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
|
||||||
media_player: [
|
media_player: [
|
||||||
|
@ -34,6 +34,8 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
|||||||
case "device_tracker":
|
case "device_tracker":
|
||||||
case "person":
|
case "person":
|
||||||
return compareState !== "not_home";
|
return compareState !== "not_home";
|
||||||
|
case "lawn_mower":
|
||||||
|
return ["mowing", "error"].includes(compareState);
|
||||||
case "lock":
|
case "lock":
|
||||||
return compareState !== "locked";
|
return compareState !== "locked";
|
||||||
case "media_player":
|
case "media_player":
|
||||||
|
@ -22,6 +22,7 @@ const STATE_COLORED_DOMAIN = new Set([
|
|||||||
"group",
|
"group",
|
||||||
"humidifier",
|
"humidifier",
|
||||||
"input_boolean",
|
"input_boolean",
|
||||||
|
"lawn_mower",
|
||||||
"light",
|
"light",
|
||||||
"lock",
|
"lock",
|
||||||
"media_player",
|
"media_player",
|
||||||
@ -36,6 +37,7 @@ const STATE_COLORED_DOMAIN = new Set([
|
|||||||
"timer",
|
"timer",
|
||||||
"update",
|
"update",
|
||||||
"vacuum",
|
"vacuum",
|
||||||
|
"water_heater",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const stateColorCss = (stateObj: HassEntity, state?: string) => {
|
export const stateColorCss = (stateObj: HassEntity, state?: string) => {
|
||||||
|
@ -4,7 +4,7 @@ export const clamp = (value: number, min: number, max: number) =>
|
|||||||
// Variant that only applies the clamping to a border if the border is defined
|
// Variant that only applies the clamping to a border if the border is defined
|
||||||
export const conditionalClamp = (value: number, min?: number, max?: number) => {
|
export const conditionalClamp = (value: number, min?: number, max?: number) => {
|
||||||
let result: number;
|
let result: number;
|
||||||
result = min ? Math.max(value, min) : value;
|
result = min != null ? Math.max(value, min) : value;
|
||||||
result = max ? Math.min(result, max) : result;
|
result = max != null ? Math.min(result, max) : result;
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
52
src/common/translations/entity-state.ts
Normal file
52
src/common/translations/entity-state.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import type { FrontendLocaleData } from "../../data/translation";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import type { LocalizeFunc } from "./localize";
|
||||||
|
|
||||||
|
export type FormatEntityStateFunc = (
|
||||||
|
stateObj: HassEntity,
|
||||||
|
state?: string
|
||||||
|
) => string;
|
||||||
|
export type FormatEntityAttributeValueFunc = (
|
||||||
|
stateObj: HassEntity,
|
||||||
|
attribute: string,
|
||||||
|
value?: any
|
||||||
|
) => string;
|
||||||
|
export type formatEntityAttributeNameFunc = (
|
||||||
|
stateObj: HassEntity,
|
||||||
|
attribute: string
|
||||||
|
) => string;
|
||||||
|
|
||||||
|
export const computeFormatFunctions = async (
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig,
|
||||||
|
entities: HomeAssistant["entities"]
|
||||||
|
): Promise<{
|
||||||
|
formatEntityState: FormatEntityStateFunc;
|
||||||
|
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
|
||||||
|
formatEntityAttributeName: formatEntityAttributeNameFunc;
|
||||||
|
}> => {
|
||||||
|
const { computeStateDisplay } = await import(
|
||||||
|
"../entity/compute_state_display"
|
||||||
|
);
|
||||||
|
const { computeAttributeValueDisplay, computeAttributeNameDisplay } =
|
||||||
|
await import("../entity/compute_attribute_display");
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatEntityState: (stateObj, state) =>
|
||||||
|
computeStateDisplay(localize, stateObj, locale, config, entities, state),
|
||||||
|
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||||
|
computeAttributeValueDisplay(
|
||||||
|
localize,
|
||||||
|
stateObj,
|
||||||
|
locale,
|
||||||
|
config,
|
||||||
|
entities,
|
||||||
|
attribute,
|
||||||
|
value
|
||||||
|
),
|
||||||
|
formatEntityAttributeName: (stateObj, attribute) =>
|
||||||
|
computeAttributeNameDisplay(localize, stateObj, entities, attribute),
|
||||||
|
};
|
||||||
|
};
|
@ -11,10 +11,12 @@ export type LocalizeKeys =
|
|||||||
| `ui.card.alarm_control_panel.${string}`
|
| `ui.card.alarm_control_panel.${string}`
|
||||||
| `ui.card.weather.attributes.${string}`
|
| `ui.card.weather.attributes.${string}`
|
||||||
| `ui.card.weather.cardinal_direction.${string}`
|
| `ui.card.weather.cardinal_direction.${string}`
|
||||||
|
| `ui.card.lawn_mower.actions.${string}`
|
||||||
| `ui.components.calendar.event.rrule.${string}`
|
| `ui.components.calendar.event.rrule.${string}`
|
||||||
| `ui.components.logbook.${string}`
|
| `ui.components.logbook.${string}`
|
||||||
| `ui.components.selectors.file.${string}`
|
| `ui.components.selectors.file.${string}`
|
||||||
| `ui.dialogs.entity_registry.editor.${string}`
|
| `ui.dialogs.entity_registry.editor.${string}`
|
||||||
|
| `ui.dialogs.more_info_control.lawn_mower.${string}`
|
||||||
| `ui.dialogs.more_info_control.vacuum.${string}`
|
| `ui.dialogs.more_info_control.vacuum.${string}`
|
||||||
| `ui.dialogs.quick-bar.commands.${string}`
|
| `ui.dialogs.quick-bar.commands.${string}`
|
||||||
| `ui.dialogs.unhealthy.reason.${string}`
|
| `ui.dialogs.unhealthy.reason.${string}`
|
||||||
|
@ -1,100 +0,0 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
|
||||||
import { EventsMixin } from "../../mixins/events-mixin";
|
|
||||||
import "./ha-progress-button";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @appliesMixin EventsMixin
|
|
||||||
*/
|
|
||||||
class HaCallServiceButton extends EventsMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<ha-progress-button
|
|
||||||
id="progress"
|
|
||||||
progress="[[progress]]"
|
|
||||||
disabled="[[disabled]]"
|
|
||||||
on-click="buttonTapped"
|
|
||||||
tabindex="0"
|
|
||||||
><slot></slot
|
|
||||||
></ha-progress-button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
progress: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
domain: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
service: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
serviceData: {
|
|
||||||
type: Object,
|
|
||||||
value: {},
|
|
||||||
},
|
|
||||||
|
|
||||||
confirmation: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
callService() {
|
|
||||||
this.progress = true;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
||||||
const el = this;
|
|
||||||
const eventData = {
|
|
||||||
domain: this.domain,
|
|
||||||
service: this.service,
|
|
||||||
serviceData: this.serviceData,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.hass
|
|
||||||
.callService(this.domain, this.service, this.serviceData)
|
|
||||||
.then(
|
|
||||||
() => {
|
|
||||||
el.progress = false;
|
|
||||||
el.$.progress.actionSuccess();
|
|
||||||
eventData.success = true;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
el.progress = false;
|
|
||||||
el.$.progress.actionError();
|
|
||||||
eventData.success = false;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
el.fire("hass-service-called", eventData);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonTapped() {
|
|
||||||
if (this.confirmation) {
|
|
||||||
showConfirmationDialog(this, {
|
|
||||||
text: this.confirmation,
|
|
||||||
confirm: () => this.callService(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.callService();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("ha-call-service-button", HaCallServiceButton);
|
|
92
src/components/buttons/ha-call-service-button.ts
Normal file
92
src/components/buttons/ha-call-service-button.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { LitElement, TemplateResult, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||||
|
import "./ha-progress-button";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
|
||||||
|
@customElement("ha-call-service-button")
|
||||||
|
class HaCallServiceButton extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public progress = false;
|
||||||
|
|
||||||
|
@property() public domain!: string;
|
||||||
|
|
||||||
|
@property() public service!: string;
|
||||||
|
|
||||||
|
@property({ type: Object }) public serviceData = {};
|
||||||
|
|
||||||
|
@property() public confirmation?;
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-progress-button
|
||||||
|
.progress=${this.progress}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
@click=${this._buttonTapped}
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<slot></slot
|
||||||
|
></ha-progress-button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _callService() {
|
||||||
|
this.progress = true;
|
||||||
|
const eventData = {
|
||||||
|
domain: this.domain,
|
||||||
|
service: this.service,
|
||||||
|
serviceData: this.serviceData,
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const progressElement =
|
||||||
|
this.shadowRoot!.querySelector("ha-progress-button")!;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.hass.callService(this.domain, this.service, this.serviceData);
|
||||||
|
this.progress = false;
|
||||||
|
progressElement.actionSuccess();
|
||||||
|
eventData.success = true;
|
||||||
|
} catch (e) {
|
||||||
|
this.progress = false;
|
||||||
|
progressElement.actionError();
|
||||||
|
eventData.success = false;
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
fireEvent(this, "hass-service-called", eventData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buttonTapped() {
|
||||||
|
if (this.confirmation) {
|
||||||
|
showConfirmationDialog(this, {
|
||||||
|
text: this.confirmation,
|
||||||
|
confirm: () => this._callService(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._callService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-call-service-button": HaCallServiceButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// for fire event
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"hass-service-called": {
|
||||||
|
domain: string;
|
||||||
|
service: string;
|
||||||
|
serviceData: object;
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -15,13 +15,20 @@ import { HomeAssistant } from "../../types";
|
|||||||
|
|
||||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||||
|
|
||||||
interface Tooltip extends TooltipModel<any> {
|
export interface ChartResizeOptions {
|
||||||
|
aspectRatio?: number;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tooltip
|
||||||
|
extends Omit<TooltipModel<any>, "tooltipPosition" | "hasValue" | "getProps"> {
|
||||||
top: string;
|
top: string;
|
||||||
left: string;
|
left: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ha-chart-base")
|
@customElement("ha-chart-base")
|
||||||
export default class HaChartBase extends LitElement {
|
export class HaChartBase extends LitElement {
|
||||||
public chart?: Chart;
|
public chart?: Chart;
|
||||||
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -45,14 +52,6 @@ export default class HaChartBase extends LitElement {
|
|||||||
|
|
||||||
@state() private _hiddenDatasets: Set<number> = new Set();
|
@state() private _hiddenDatasets: Set<number> = new Set();
|
||||||
|
|
||||||
private _releaseCanvas() {
|
|
||||||
// release the canvas memory to prevent
|
|
||||||
// safari from running out of memory.
|
|
||||||
if (this.chart) {
|
|
||||||
this.chart.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
this._releaseCanvas();
|
this._releaseCanvas();
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
@ -65,6 +64,36 @@ export default class HaChartBase extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updateChart = (
|
||||||
|
mode:
|
||||||
|
| "resize"
|
||||||
|
| "reset"
|
||||||
|
| "none"
|
||||||
|
| "hide"
|
||||||
|
| "show"
|
||||||
|
| "default"
|
||||||
|
| "active"
|
||||||
|
| undefined
|
||||||
|
): void => {
|
||||||
|
this.chart?.update(mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
public resize = (options?: ChartResizeOptions): void => {
|
||||||
|
if (options?.aspectRatio && !options.height) {
|
||||||
|
options.height = Math.round(
|
||||||
|
(options.width ?? this.clientWidth) / options.aspectRatio
|
||||||
|
);
|
||||||
|
} else if (options?.aspectRatio && !options.width) {
|
||||||
|
options.width = Math.round(
|
||||||
|
(options.height ?? this.clientHeight) * options.aspectRatio
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.chart?.resize(
|
||||||
|
options?.width ?? this.clientWidth,
|
||||||
|
options?.height ?? this.clientHeight
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
this._setupChart();
|
this._setupChart();
|
||||||
this.data.datasets.forEach((dataset, index) => {
|
this.data.datasets.forEach((dataset, index) => {
|
||||||
@ -80,14 +109,11 @@ export default class HaChartBase extends LitElement {
|
|||||||
if (!this.hasUpdated || !this.chart) {
|
if (!this.hasUpdated || !this.chart) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (changedProps.has("plugins")) {
|
if (changedProps.has("plugins") || changedProps.has("chartType")) {
|
||||||
this.chart.destroy();
|
this.chart.destroy();
|
||||||
this._setupChart();
|
this._setupChart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (changedProps.has("chartType")) {
|
|
||||||
this.chart.config.type = this.chartType;
|
|
||||||
}
|
|
||||||
if (changedProps.has("data")) {
|
if (changedProps.has("data")) {
|
||||||
if (this._hiddenDatasets.size) {
|
if (this._hiddenDatasets.size) {
|
||||||
this.data.datasets.forEach((dataset, index) => {
|
this.data.datasets.forEach((dataset, index) => {
|
||||||
@ -131,55 +157,70 @@ export default class HaChartBase extends LitElement {
|
|||||||
</div>`
|
</div>`
|
||||||
: ""}
|
: ""}
|
||||||
<div
|
<div
|
||||||
class="chartContainer"
|
class="animationContainer"
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
height: `${this.height ?? this._chartHeight}px`,
|
height: `${this.height || this._chartHeight || 0}px`,
|
||||||
overflow: this._chartHeight ? "initial" : "hidden",
|
overflow: this._chartHeight ? "initial" : "hidden",
|
||||||
"padding-left": `${computeRTL(this.hass) ? 0 : this.paddingYAxis}px`,
|
|
||||||
"padding-right": `${computeRTL(this.hass) ? this.paddingYAxis : 0}px`,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<canvas></canvas>
|
<div
|
||||||
${this._tooltip
|
class="chartContainer"
|
||||||
? html`<div
|
style=${styleMap({
|
||||||
class="chartTooltip ${classMap({ [this._tooltip.yAlign]: true })}"
|
height: `${
|
||||||
style=${styleMap({
|
this.height ?? this._chartHeight ?? this.clientWidth / 2
|
||||||
top: this._tooltip.top,
|
}px`,
|
||||||
left: this._tooltip.left,
|
"padding-left": `${
|
||||||
})}
|
computeRTL(this.hass) ? 0 : this.paddingYAxis
|
||||||
>
|
}px`,
|
||||||
<div class="title">${this._tooltip.title}</div>
|
"padding-right": `${
|
||||||
${this._tooltip.beforeBody
|
computeRTL(this.hass) ? this.paddingYAxis : 0
|
||||||
? html`<div class="beforeBody">
|
}px`,
|
||||||
${this._tooltip.beforeBody}
|
})}
|
||||||
</div>`
|
>
|
||||||
: ""}
|
<canvas></canvas>
|
||||||
<div>
|
${this._tooltip
|
||||||
<ul>
|
? html`<div
|
||||||
${this._tooltip.body.map(
|
class="chartTooltip ${classMap({
|
||||||
(item, i) =>
|
[this._tooltip.yAlign]: true,
|
||||||
html`<li>
|
})}"
|
||||||
<div
|
style=${styleMap({
|
||||||
class="bullet"
|
top: this._tooltip.top,
|
||||||
style=${styleMap({
|
left: this._tooltip.left,
|
||||||
backgroundColor: this._tooltip!.labelColors[i]
|
})}
|
||||||
.backgroundColor as string,
|
>
|
||||||
borderColor: this._tooltip!.labelColors[i]
|
<div class="title">${this._tooltip.title}</div>
|
||||||
.borderColor as string,
|
${this._tooltip.beforeBody
|
||||||
})}
|
? html`<div class="beforeBody">
|
||||||
></div>
|
${this._tooltip.beforeBody}
|
||||||
${item.lines.join("\n")}
|
</div>`
|
||||||
</li>`
|
: ""}
|
||||||
)}
|
<div>
|
||||||
</ul>
|
<ul>
|
||||||
</div>
|
${this._tooltip.body.map(
|
||||||
${this._tooltip.footer.length
|
(item, i) =>
|
||||||
? html`<div class="footer">
|
html`<li>
|
||||||
${this._tooltip.footer.map((item) => html`${item}<br />`)}
|
<div
|
||||||
</div>`
|
class="bullet"
|
||||||
: ""}
|
style=${styleMap({
|
||||||
</div>`
|
backgroundColor: this._tooltip!.labelColors[i]
|
||||||
: ""}
|
.backgroundColor as string,
|
||||||
|
borderColor: this._tooltip!.labelColors[i]
|
||||||
|
.borderColor as string,
|
||||||
|
})}
|
||||||
|
></div>
|
||||||
|
${item.lines.join("\n")}
|
||||||
|
</li>`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
${this._tooltip.footer.length
|
||||||
|
? html`<div class="footer">
|
||||||
|
${this._tooltip.footer.map((item) => html`${item}<br />`)}
|
||||||
|
</div>`
|
||||||
|
: ""}
|
||||||
|
</div>`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -213,6 +254,7 @@ export default class HaChartBase extends LitElement {
|
|||||||
|
|
||||||
private _createOptions() {
|
private _createOptions() {
|
||||||
return {
|
return {
|
||||||
|
maintainAspectRatio: false,
|
||||||
...this.options,
|
...this.options,
|
||||||
plugins: {
|
plugins: {
|
||||||
...this.options?.plugins,
|
...this.options?.plugins,
|
||||||
@ -233,10 +275,10 @@ export default class HaChartBase extends LitElement {
|
|||||||
return [
|
return [
|
||||||
...(this.plugins || []),
|
...(this.plugins || []),
|
||||||
{
|
{
|
||||||
id: "afterRenderHook",
|
id: "resizeHook",
|
||||||
afterRender: (chart) => {
|
resize: (chart) => {
|
||||||
const change = chart.height - (this._chartHeight ?? 0);
|
const change = chart.height - (this._chartHeight ?? 0);
|
||||||
if (!this._chartHeight || change > 0 || change < -12) {
|
if (!this._chartHeight || change > 12 || change < -12) {
|
||||||
// hysteresis to prevent infinite render loops
|
// hysteresis to prevent infinite render loops
|
||||||
this._chartHeight = chart.height;
|
this._chartHeight = chart.height;
|
||||||
}
|
}
|
||||||
@ -288,21 +330,13 @@ export default class HaChartBase extends LitElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateChart = (
|
private _releaseCanvas() {
|
||||||
mode:
|
// release the canvas memory to prevent
|
||||||
| "resize"
|
// safari from running out of memory.
|
||||||
| "reset"
|
|
||||||
| "none"
|
|
||||||
| "hide"
|
|
||||||
| "show"
|
|
||||||
| "normal"
|
|
||||||
| "active"
|
|
||||||
| undefined
|
|
||||||
): void => {
|
|
||||||
if (this.chart) {
|
if (this.chart) {
|
||||||
this.chart.update(mode);
|
this.chart.destroy();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
@ -310,11 +344,14 @@ export default class HaChartBase extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
position: var(--chart-base-position, relative);
|
position: var(--chart-base-position, relative);
|
||||||
}
|
}
|
||||||
.chartContainer {
|
.animationContainer {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 0;
|
height: 0;
|
||||||
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
.chartContainer {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
canvas {
|
canvas {
|
||||||
max-height: var(--chart-max-height, 400px);
|
max-height: var(--chart-max-height, 400px);
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,28 @@
|
|||||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||||
import { html, LitElement, PropertyValues } from "lit";
|
import { html, LitElement, PropertyValues } from "lit";
|
||||||
import { property, state } from "lit/decorators";
|
import { property, query, state } from "lit/decorators";
|
||||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
import {
|
import {
|
||||||
formatNumber,
|
formatNumber,
|
||||||
numberFormatToLocale,
|
numberFormatToLocale,
|
||||||
|
getNumberFormatOptions,
|
||||||
} from "../../common/number/format_number";
|
} from "../../common/number/format_number";
|
||||||
import { LineChartEntity, LineChartState } from "../../data/history";
|
import { LineChartEntity, LineChartState } from "../../data/history";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
import {
|
||||||
|
ChartResizeOptions,
|
||||||
|
HaChartBase,
|
||||||
|
MIN_TIME_BETWEEN_UPDATES,
|
||||||
|
} from "./ha-chart-base";
|
||||||
|
|
||||||
const safeParseFloat = (value) => {
|
const safeParseFloat = (value) => {
|
||||||
const parsed = parseFloat(value);
|
const parsed = parseFloat(value);
|
||||||
return isFinite(parsed) ? parsed : null;
|
return isFinite(parsed) ? parsed : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
class StateHistoryChartLine extends LitElement {
|
export class StateHistoryChartLine extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public data: LineChartEntity[] = [];
|
@property({ attribute: false }) public data: LineChartEntity[] = [];
|
||||||
@ -46,6 +51,12 @@ class StateHistoryChartLine extends LitElement {
|
|||||||
|
|
||||||
private _chartTime: Date = new Date();
|
private _chartTime: Date = new Date();
|
||||||
|
|
||||||
|
@query("ha-chart-base") private _chart?: HaChartBase;
|
||||||
|
|
||||||
|
public resize = (options?: ChartResizeOptions): void => {
|
||||||
|
this._chart?.resize(options);
|
||||||
|
};
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-chart-base
|
<ha-chart-base
|
||||||
@ -125,7 +136,13 @@ class StateHistoryChartLine extends LitElement {
|
|||||||
label: (context) =>
|
label: (context) =>
|
||||||
`${context.dataset.label}: ${formatNumber(
|
`${context.dataset.label}: ${formatNumber(
|
||||||
context.parsed.y,
|
context.parsed.y,
|
||||||
this.hass.locale
|
this.hass.locale,
|
||||||
|
getNumberFormatOptions(
|
||||||
|
this.hass.states[this.data[context.datasetIndex].entity_id],
|
||||||
|
this.hass.entities[
|
||||||
|
this.data[context.datasetIndex].entity_id
|
||||||
|
]
|
||||||
|
)
|
||||||
)} ${this.unit}`,
|
)} ${this.unit}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||||
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
|
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
@ -8,7 +8,11 @@ import { numberFormatToLocale } from "../../common/number/format_number";
|
|||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
import { TimelineEntity } from "../../data/history";
|
import { TimelineEntity } from "../../data/history";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
import {
|
||||||
|
ChartResizeOptions,
|
||||||
|
HaChartBase,
|
||||||
|
MIN_TIME_BETWEEN_UPDATES,
|
||||||
|
} from "./ha-chart-base";
|
||||||
import type { TimeLineData } from "./timeline-chart/const";
|
import type { TimeLineData } from "./timeline-chart/const";
|
||||||
import { computeTimelineColor } from "./timeline-chart/timeline-color";
|
import { computeTimelineColor } from "./timeline-chart/timeline-color";
|
||||||
|
|
||||||
@ -46,6 +50,12 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
|
|
||||||
private _chartTime: Date = new Date();
|
private _chartTime: Date = new Date();
|
||||||
|
|
||||||
|
@query("ha-chart-base") private _chart?: HaChartBase;
|
||||||
|
|
||||||
|
public resize = (options?: ChartResizeOptions): void => {
|
||||||
|
this._chart?.resize(options);
|
||||||
|
};
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-chart-base
|
<ha-chart-base
|
||||||
|
@ -6,7 +6,13 @@ import {
|
|||||||
nothing,
|
nothing,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, eventOptions, property, state } from "lit/decorators";
|
import {
|
||||||
|
customElement,
|
||||||
|
eventOptions,
|
||||||
|
property,
|
||||||
|
queryAll,
|
||||||
|
state,
|
||||||
|
} from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||||
import {
|
import {
|
||||||
@ -18,6 +24,9 @@ import { loadVirtualizer } from "../../resources/virtualizer";
|
|||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "./state-history-chart-line";
|
import "./state-history-chart-line";
|
||||||
import "./state-history-chart-timeline";
|
import "./state-history-chart-timeline";
|
||||||
|
import type { StateHistoryChartLine } from "./state-history-chart-line";
|
||||||
|
import type { StateHistoryChartTimeline } from "./state-history-chart-timeline";
|
||||||
|
import { ChartResizeOptions } from "./ha-chart-base";
|
||||||
|
|
||||||
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
|
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
|
||||||
|
|
||||||
@ -75,6 +84,16 @@ export class StateHistoryCharts extends LitElement {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@restoreScroll(".container") private _savedScrollPos?: number;
|
@restoreScroll(".container") private _savedScrollPos?: number;
|
||||||
|
|
||||||
|
@queryAll("state-history-chart-line, state-history-chart-timeline")
|
||||||
|
private _charts?: StateHistoryChartLine[] | StateHistoryChartTimeline[];
|
||||||
|
|
||||||
|
public resize = (options?: ChartResizeOptions): void => {
|
||||||
|
this._charts?.forEach(
|
||||||
|
(chart: StateHistoryChartLine | StateHistoryChartTimeline) =>
|
||||||
|
chart.resize(options)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!isComponentLoaded(this.hass, "history")) {
|
if (!isComponentLoaded(this.hass, "history")) {
|
||||||
return html`<div class="info">
|
return html`<div class="info">
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state, query } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
@ -31,6 +31,7 @@ import {
|
|||||||
} from "../../data/recorder";
|
} from "../../data/recorder";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "./ha-chart-base";
|
import "./ha-chart-base";
|
||||||
|
import type { ChartResizeOptions, HaChartBase } from "./ha-chart-base";
|
||||||
|
|
||||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||||
mean: "mean",
|
mean: "mean",
|
||||||
@ -42,7 +43,7 @@ export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@customElement("statistics-chart")
|
@customElement("statistics-chart")
|
||||||
class StatisticsChart extends LitElement {
|
export class StatisticsChart extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public statisticsData?: Statistics;
|
@property({ attribute: false }) public statisticsData?: Statistics;
|
||||||
@ -75,8 +76,14 @@ class StatisticsChart extends LitElement {
|
|||||||
|
|
||||||
@state() private _chartOptions?: ChartOptions;
|
@state() private _chartOptions?: ChartOptions;
|
||||||
|
|
||||||
|
@query("ha-chart-base") private _chart?: HaChartBase;
|
||||||
|
|
||||||
private _computedStyle?: CSSStyleDeclaration;
|
private _computedStyle?: CSSStyleDeclaration;
|
||||||
|
|
||||||
|
public resize = (options?: ChartResizeOptions): void => {
|
||||||
|
this._chart?.resize(options);
|
||||||
|
};
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
return changedProps.size > 1 || !changedProps.has("hass");
|
return changedProps.size > 1 || !changedProps.has("hass");
|
||||||
}
|
}
|
||||||
@ -329,8 +336,14 @@ class StatisticsChart extends LitElement {
|
|||||||
|
|
||||||
const statTypes: this["statTypes"] = [];
|
const statTypes: this["statTypes"] = [];
|
||||||
|
|
||||||
const drawBands =
|
const hasMean =
|
||||||
this.statTypes.includes("mean") && statisticsHaveType(stats, "mean");
|
this.statTypes.includes("mean") && statisticsHaveType(stats, "mean");
|
||||||
|
const drawBands =
|
||||||
|
hasMean ||
|
||||||
|
(this.statTypes.includes("min") &&
|
||||||
|
statisticsHaveType(stats, "min") &&
|
||||||
|
this.statTypes.includes("max") &&
|
||||||
|
statisticsHaveType(stats, "max"));
|
||||||
|
|
||||||
const sortedTypes = drawBands
|
const sortedTypes = drawBands
|
||||||
? [...this.statTypes].sort((a, b) => {
|
? [...this.statTypes].sort((a, b) => {
|
||||||
@ -358,13 +371,14 @@ class StatisticsChart extends LitElement {
|
|||||||
`ui.components.statistics_charts.statistic_types.${type}`
|
`ui.components.statistics_charts.statistic_types.${type}`
|
||||||
),
|
),
|
||||||
fill: drawBands
|
fill: drawBands
|
||||||
? type === "min"
|
? type === "min" && hasMean
|
||||||
? "+1"
|
? "+1"
|
||||||
: type === "max"
|
: type === "max"
|
||||||
? "-1"
|
? "-1"
|
||||||
: false
|
: false
|
||||||
: false,
|
: false,
|
||||||
borderColor: band ? color + (this.hideLegend ? "00" : "7F") : color,
|
borderColor:
|
||||||
|
band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color,
|
||||||
backgroundColor: band ? color + "3F" : color + "7F",
|
backgroundColor: band ? color + "3F" : color + "7F",
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
data: [],
|
data: [],
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
import type {
|
||||||
|
BarControllerChartOptions,
|
||||||
|
BarControllerDatasetOptions,
|
||||||
|
} from "chart.js";
|
||||||
|
|
||||||
export interface TimeLineData {
|
export interface TimeLineData {
|
||||||
start: Date;
|
start: Date;
|
||||||
end: Date;
|
end: Date;
|
||||||
|
@ -16,7 +16,7 @@ export interface TextBaroptions extends BarOptions {
|
|||||||
export class TextBarElement extends BarElement {
|
export class TextBarElement extends BarElement {
|
||||||
static id = "textbar";
|
static id = "textbar";
|
||||||
|
|
||||||
draw(ctx) {
|
draw(ctx: CanvasRenderingContext2D) {
|
||||||
super.draw(ctx);
|
super.draw(ctx);
|
||||||
const options = this.options as TextBaroptions;
|
const options = this.options as TextBaroptions;
|
||||||
const { x, y, base, width, text } = (
|
const { x, y, base, width, text } = (
|
||||||
|
@ -2,6 +2,95 @@ import { BarController, BarElement } from "chart.js";
|
|||||||
import { TimeLineData } from "./const";
|
import { TimeLineData } from "./const";
|
||||||
import { TextBarProps } from "./textbar-element";
|
import { TextBarProps } from "./textbar-element";
|
||||||
|
|
||||||
|
function borderProps(properties) {
|
||||||
|
let reverse;
|
||||||
|
let start;
|
||||||
|
let end;
|
||||||
|
let top;
|
||||||
|
let bottom;
|
||||||
|
if (properties.horizontal) {
|
||||||
|
reverse = properties.base > properties.x;
|
||||||
|
start = "left";
|
||||||
|
end = "right";
|
||||||
|
} else {
|
||||||
|
reverse = properties.base < properties.y;
|
||||||
|
start = "bottom";
|
||||||
|
end = "top";
|
||||||
|
}
|
||||||
|
if (reverse) {
|
||||||
|
top = "end";
|
||||||
|
bottom = "start";
|
||||||
|
} else {
|
||||||
|
top = "start";
|
||||||
|
bottom = "end";
|
||||||
|
}
|
||||||
|
return { start, end, reverse, top, bottom };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBorderSkipped(properties, options, stack, index) {
|
||||||
|
let edge = options.borderSkipped;
|
||||||
|
const res = {};
|
||||||
|
|
||||||
|
if (!edge) {
|
||||||
|
properties.borderSkipped = res;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edge === true) {
|
||||||
|
properties.borderSkipped = {
|
||||||
|
top: true,
|
||||||
|
right: true,
|
||||||
|
bottom: true,
|
||||||
|
left: true,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { start, end, reverse, top, bottom } = borderProps(properties);
|
||||||
|
|
||||||
|
if (edge === "middle" && stack) {
|
||||||
|
properties.enableBorderRadius = true;
|
||||||
|
if ((stack._top || 0) === index) {
|
||||||
|
edge = top;
|
||||||
|
} else if ((stack._bottom || 0) === index) {
|
||||||
|
edge = bottom;
|
||||||
|
} else {
|
||||||
|
res[parseEdge(bottom, start, end, reverse)] = true;
|
||||||
|
edge = top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res[parseEdge(edge, start, end, reverse)] = true;
|
||||||
|
properties.borderSkipped = res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEdge(edge, a, b, reverse) {
|
||||||
|
if (reverse) {
|
||||||
|
edge = swap(edge, a, b);
|
||||||
|
edge = startEnd(edge, b, a);
|
||||||
|
} else {
|
||||||
|
edge = startEnd(edge, a, b);
|
||||||
|
}
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
function swap(orig, v1, v2) {
|
||||||
|
return orig === v1 ? v2 : orig === v2 ? v1 : orig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEnd(v, start, end) {
|
||||||
|
return v === "start" ? start : v === "end" ? end : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInflateAmount(
|
||||||
|
properties,
|
||||||
|
{ inflateAmount }: { inflateAmount?: string | number },
|
||||||
|
ratio
|
||||||
|
) {
|
||||||
|
properties.inflateAmount =
|
||||||
|
inflateAmount === "auto" ? (ratio === 1 ? 0.33 : 0) : inflateAmount;
|
||||||
|
}
|
||||||
|
|
||||||
function parseValue(entry, item, vScale, i) {
|
function parseValue(entry, item, vScale, i) {
|
||||||
const startValue = vScale.parse(entry.start, i);
|
const startValue = vScale.parse(entry.start, i);
|
||||||
const endValue = vScale.parse(entry.end, i);
|
const endValue = vScale.parse(entry.end, i);
|
||||||
@ -97,7 +186,7 @@ export class TimelineController extends BarController {
|
|||||||
bars: BarElement[],
|
bars: BarElement[],
|
||||||
start: number,
|
start: number,
|
||||||
count: number,
|
count: number,
|
||||||
mode: "reset" | "resize" | "none" | "hide" | "show" | "normal" | "active"
|
mode: "reset" | "resize" | "none" | "hide" | "show" | "default" | "active"
|
||||||
) {
|
) {
|
||||||
const vScale = this._cachedMeta.vScale!;
|
const vScale = this._cachedMeta.vScale!;
|
||||||
const iScale = this._cachedMeta.iScale!;
|
const iScale = this._cachedMeta.iScale!;
|
||||||
@ -114,15 +203,15 @@ export class TimelineController extends BarController {
|
|||||||
for (let index = start; index < start + count; index++) {
|
for (let index = start; index < start + count; index++) {
|
||||||
const data = dataset.data[index] as TimeLineData;
|
const data = dataset.data[index] as TimeLineData;
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const y = vScale.getPixelForValue(this.index);
|
const y = vScale.getPixelForValue(this.index);
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const xStart = iScale.getPixelForValue(data.start.getTime());
|
const xStart = iScale.getPixelForValue(data.start.getTime());
|
||||||
// @ts-ignore
|
|
||||||
const xEnd = iScale.getPixelForValue(data.end.getTime());
|
const xEnd = iScale.getPixelForValue(data.end.getTime());
|
||||||
const width = xEnd - xStart;
|
const width = xEnd - xStart;
|
||||||
|
|
||||||
|
const parsed = this.getParsed(index);
|
||||||
|
const stack = (parsed._stacks || {})[vScale.axis];
|
||||||
|
|
||||||
const height = 10;
|
const height = 10;
|
||||||
|
|
||||||
const properties: TextBarProps = {
|
const properties: TextBarProps = {
|
||||||
@ -145,7 +234,10 @@ export class TimelineController extends BarController {
|
|||||||
backgroundColor: data.color,
|
backgroundColor: data.color,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const options = properties.options || bars[index].options;
|
||||||
|
|
||||||
|
setBorderSkipped(properties, options, stack, index);
|
||||||
|
setInflateAmount(properties, options, 1);
|
||||||
this.updateElement(bars[index], index, properties as any, mode);
|
this.updateElement(bars[index], index, properties as any, mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -324,6 +324,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
.renderer=${rowRenderer}
|
.renderer=${rowRenderer}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
|
item-id-path="id"
|
||||||
item-value-path="id"
|
item-value-path="id"
|
||||||
item-label-path="name"
|
item-label-path="name"
|
||||||
@opened-changed=${this._openedChanged}
|
@opened-changed=${this._openedChanged}
|
||||||
|
@ -62,6 +62,8 @@ export class HaStateLabelBadge extends LitElement {
|
|||||||
|
|
||||||
@property() public image?: string;
|
@property() public image?: string;
|
||||||
|
|
||||||
|
@property() public showName?: boolean;
|
||||||
|
|
||||||
@state() private _timerTimeRemaining?: number;
|
@state() private _timerTimeRemaining?: number;
|
||||||
|
|
||||||
private _connected?: boolean;
|
private _connected?: boolean;
|
||||||
@ -132,7 +134,9 @@ export class HaStateLabelBadge extends LitElement {
|
|||||||
entityState,
|
entityState,
|
||||||
this._timerTimeRemaining
|
this._timerTimeRemaining
|
||||||
)}
|
)}
|
||||||
.description=${this.name ?? computeStateName(entityState)}
|
.description=${this.showName === false
|
||||||
|
? undefined
|
||||||
|
: this.name ?? computeStateName(entityState)}
|
||||||
>
|
>
|
||||||
${!image && showIcon
|
${!image && showIcon
|
||||||
? html`<ha-state-icon
|
? html`<ha-state-icon
|
||||||
|
@ -79,6 +79,14 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
@property({ type: Boolean, attribute: "entities-only" })
|
@property({ type: Boolean, attribute: "entities-only" })
|
||||||
public entitiesOnly = false;
|
public entitiesOnly = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of statistics to be excluded.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-statistics
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-statistics" })
|
||||||
|
public excludeStatistics?: string[];
|
||||||
|
|
||||||
@state() private _opened?: boolean;
|
@state() private _opened?: boolean;
|
||||||
|
|
||||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||||
@ -118,7 +126,8 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
includeStatisticsUnitOfMeasurement?: string | string[],
|
includeStatisticsUnitOfMeasurement?: string | string[],
|
||||||
includeUnitClass?: string | string[],
|
includeUnitClass?: string | string[],
|
||||||
includeDeviceClass?: string | string[],
|
includeDeviceClass?: string | string[],
|
||||||
entitiesOnly?: boolean
|
entitiesOnly?: boolean,
|
||||||
|
excludeStatistics?: string[]
|
||||||
): StatisticItem[] => {
|
): StatisticItem[] => {
|
||||||
if (!statisticIds.length) {
|
if (!statisticIds.length) {
|
||||||
return [
|
return [
|
||||||
@ -163,6 +172,12 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
|
|
||||||
const output: StatisticItem[] = [];
|
const output: StatisticItem[] = [];
|
||||||
statisticIds.forEach((meta) => {
|
statisticIds.forEach((meta) => {
|
||||||
|
if (
|
||||||
|
excludeStatistics &&
|
||||||
|
excludeStatistics.includes(meta.statistic_id)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const entityState = this.hass.states[meta.statistic_id];
|
const entityState = this.hass.states[meta.statistic_id];
|
||||||
if (!entityState) {
|
if (!entityState) {
|
||||||
if (!entitiesOnly) {
|
if (!entitiesOnly) {
|
||||||
@ -240,7 +255,8 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
this.includeStatisticsUnitOfMeasurement,
|
this.includeStatisticsUnitOfMeasurement,
|
||||||
this.includeUnitClass,
|
this.includeUnitClass,
|
||||||
this.includeDeviceClass,
|
this.includeDeviceClass,
|
||||||
this.entitiesOnly
|
this.entitiesOnly,
|
||||||
|
this.excludeStatistics
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.updateComplete.then(() => {
|
this.updateComplete.then(() => {
|
||||||
@ -249,7 +265,8 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
this.includeStatisticsUnitOfMeasurement,
|
this.includeStatisticsUnitOfMeasurement,
|
||||||
this.includeUnitClass,
|
this.includeUnitClass,
|
||||||
this.includeDeviceClass,
|
this.includeDeviceClass,
|
||||||
this.entitiesOnly
|
this.entitiesOnly,
|
||||||
|
this.excludeStatistics
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import {
|
|||||||
} from "../../common/entity/state_color";
|
} from "../../common/entity/state_color";
|
||||||
import { iconColorCSS } from "../../common/style/icon_color_css";
|
import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||||
import { HVAC_ACTION_TO_MODE } from "../../data/climate";
|
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-state-icon";
|
import "../ha-state-icon";
|
||||||
|
|
||||||
@ -160,10 +160,10 @@ export class StateBadge extends LitElement {
|
|||||||
}
|
}
|
||||||
if (stateObj.attributes.hvac_action) {
|
if (stateObj.attributes.hvac_action) {
|
||||||
const hvacAction = stateObj.attributes.hvac_action;
|
const hvacAction = stateObj.attributes.hvac_action;
|
||||||
if (hvacAction in HVAC_ACTION_TO_MODE) {
|
if (hvacAction in CLIMATE_HVAC_ACTION_TO_MODE) {
|
||||||
iconStyle.color = stateColorCss(
|
iconStyle.color = stateColorCss(
|
||||||
stateObj,
|
stateObj,
|
||||||
HVAC_ACTION_TO_MODE[hvacAction]
|
CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]
|
||||||
)!;
|
)!;
|
||||||
} else {
|
} else {
|
||||||
delete iconStyle.color;
|
delete iconStyle.color;
|
||||||
|
73
src/components/ha-attribute-value.ts
Normal file
73
src/components/ha-attribute-value.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { until } from "lit/directives/until";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import { formatNumber } from "../common/number/format_number";
|
||||||
|
|
||||||
|
let jsYamlPromise: Promise<typeof import("../resources/js-yaml-dump")>;
|
||||||
|
|
||||||
|
@customElement("ha-attribute-value")
|
||||||
|
class HaAttributeValue extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public stateObj?: HassEntity;
|
||||||
|
|
||||||
|
@property() public attribute!: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "hide-unit" })
|
||||||
|
public hideUnit?: boolean;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.stateObj) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
const attributeValue = this.stateObj.attributes[this.attribute];
|
||||||
|
|
||||||
|
if (typeof attributeValue === "number" && this.hideUnit) {
|
||||||
|
return formatNumber(attributeValue, this.hass.locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof attributeValue === "string") {
|
||||||
|
// URL handling
|
||||||
|
if (attributeValue.startsWith("http")) {
|
||||||
|
try {
|
||||||
|
// If invalid URL, exception will be raised
|
||||||
|
const url = new URL(attributeValue);
|
||||||
|
if (url.protocol === "http:" || url.protocol === "https:")
|
||||||
|
return html`
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href=${attributeValue}
|
||||||
|
>
|
||||||
|
${attributeValue}
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
} catch (_) {
|
||||||
|
// Nothing to do here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(Array.isArray(attributeValue) &&
|
||||||
|
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));
|
||||||
|
return html`<pre>${until(yaml, "")}</pre>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-attribute-value": HaAttributeValue;
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,12 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import {
|
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
||||||
computeAttributeNameDisplay,
|
|
||||||
computeAttributeValueDisplay,
|
|
||||||
} from "../common/entity/compute_attribute_display";
|
|
||||||
import { STATE_ATTRIBUTES } from "../data/entity_attributes";
|
import { STATE_ATTRIBUTES } from "../data/entity_attributes";
|
||||||
import { haStyle } from "../resources/styles";
|
import { haStyle } from "../resources/styles";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
import "./ha-expansion-panel";
|
import "./ha-expansion-panel";
|
||||||
|
import "./ha-attribute-value";
|
||||||
|
|
||||||
@customElement("ha-attributes")
|
@customElement("ha-attributes")
|
||||||
class HaAttributes extends LitElement {
|
class HaAttributes extends LitElement {
|
||||||
@ -58,14 +55,11 @@ class HaAttributes extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
${computeAttributeValueDisplay(
|
<ha-attribute-value
|
||||||
this.hass.localize,
|
.hass=${this.hass}
|
||||||
this.stateObj!,
|
.attribute=${attribute}
|
||||||
this.hass.locale,
|
.stateObj=${this.stateObj}
|
||||||
this.hass.config,
|
></ha-attribute-value>
|
||||||
this.hass.entities,
|
|
||||||
attribute
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
@ -1,138 +0,0 @@
|
|||||||
import { mdiChevronDown, mdiChevronUp } from "@mdi/js";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|
||||||
import { customElement, property, query } from "lit/decorators";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
|
||||||
import { conditionalClamp } from "../common/number/clamp";
|
|
||||||
import { HomeAssistant } from "../types";
|
|
||||||
import "./ha-icon";
|
|
||||||
import "./ha-icon-button";
|
|
||||||
|
|
||||||
@customElement("ha-climate-control")
|
|
||||||
class HaClimateControl extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public value!: number;
|
|
||||||
|
|
||||||
@property() public unit = "";
|
|
||||||
|
|
||||||
@property() public min?: number;
|
|
||||||
|
|
||||||
@property() public max?: number;
|
|
||||||
|
|
||||||
@property() public step = 1;
|
|
||||||
|
|
||||||
private _lastChanged?: number;
|
|
||||||
|
|
||||||
@query("#target_temperature") private _targetTemperature!: HTMLElement;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<div id="target_temperature">${this.value} ${this.unit}</div>
|
|
||||||
<div class="control-buttons">
|
|
||||||
<div>
|
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiChevronUp}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.climate-control.temperature_up"
|
|
||||||
)}
|
|
||||||
@click=${this._incrementValue}
|
|
||||||
>
|
|
||||||
</ha-icon-button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiChevronDown}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.climate-control.temperature_down"
|
|
||||||
)}
|
|
||||||
@click=${this._decrementValue}
|
|
||||||
>
|
|
||||||
</ha-icon-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changedProperties) {
|
|
||||||
if (changedProperties.has("value")) {
|
|
||||||
this._valueChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _temperatureStateInFlux(inFlux) {
|
|
||||||
this._targetTemperature.classList.toggle("in-flux", inFlux);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _round(value) {
|
|
||||||
// Round value to precision derived from step.
|
|
||||||
// Inspired by https://github.com/soundar24/roundSlider/blob/master/src/roundslider.js
|
|
||||||
const s = this.step.toString().split(".");
|
|
||||||
return s[1] ? parseFloat(value.toFixed(s[1].length)) : Math.round(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _incrementValue() {
|
|
||||||
const newValue = this._round(this.value + this.step);
|
|
||||||
this._processNewValue(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _decrementValue() {
|
|
||||||
const newValue = this._round(this.value - this.step);
|
|
||||||
this._processNewValue(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _processNewValue(value) {
|
|
||||||
const newValue = conditionalClamp(value, this.min, this.max);
|
|
||||||
|
|
||||||
if (this.value !== newValue) {
|
|
||||||
this.value = newValue;
|
|
||||||
this._lastChanged = Date.now();
|
|
||||||
this._temperatureStateInFlux(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _valueChanged() {
|
|
||||||
// When the last_changed timestamp is changed,
|
|
||||||
// trigger a potential event fire in the future,
|
|
||||||
// as long as last_changed is far enough in the past.
|
|
||||||
if (this._lastChanged) {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - this._lastChanged! >= 2000) {
|
|
||||||
fireEvent(this, "change");
|
|
||||||
this._temperatureStateInFlux(false);
|
|
||||||
this._lastChanged = undefined;
|
|
||||||
}
|
|
||||||
}, 2010);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return css`
|
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.in-flux {
|
|
||||||
color: var(--error-color);
|
|
||||||
}
|
|
||||||
#target_temperature {
|
|
||||||
align-self: center;
|
|
||||||
font-size: 28px;
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
.control-buttons {
|
|
||||||
font-size: 24px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
ha-icon-button {
|
|
||||||
--mdc-icon-size: 32px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-climate-control": HaClimateControl;
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,9 +2,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
|
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
|
||||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||||
import { formatNumber } from "../common/number/format_number";
|
import { CLIMATE_PRESET_NONE, ClimateEntity } from "../data/climate";
|
||||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
|
||||||
import { ClimateEntity, CLIMATE_PRESET_NONE } from "../data/climate";
|
|
||||||
import { isUnavailableState } from "../data/entity";
|
import { isUnavailableState } from "../data/entity";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
@ -54,28 +52,28 @@ class HaClimateState extends LitElement {
|
|||||||
this.stateObj.attributes.current_temperature != null &&
|
this.stateObj.attributes.current_temperature != null &&
|
||||||
this.stateObj.attributes.current_humidity != null
|
this.stateObj.attributes.current_humidity != null
|
||||||
) {
|
) {
|
||||||
return `${formatNumber(
|
return `${this.hass.formatEntityAttributeValue(
|
||||||
this.stateObj.attributes.current_temperature,
|
this.stateObj,
|
||||||
this.hass.locale
|
"current_temperature"
|
||||||
)} ${this.hass.config.unit_system.temperature}/
|
)}/
|
||||||
${formatNumber(
|
${this.hass.formatEntityAttributeValue(
|
||||||
this.stateObj.attributes.current_humidity,
|
this.stateObj,
|
||||||
this.hass.locale
|
"current_humidity"
|
||||||
)}${blankBeforePercent(this.hass.locale)}%`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.stateObj.attributes.current_temperature != null) {
|
if (this.stateObj.attributes.current_temperature != null) {
|
||||||
return `${formatNumber(
|
return this.hass.formatEntityAttributeValue(
|
||||||
this.stateObj.attributes.current_temperature,
|
this.stateObj,
|
||||||
this.hass.locale
|
"current_temperature"
|
||||||
)} ${this.hass.config.unit_system.temperature}`;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.stateObj.attributes.current_humidity != null) {
|
if (this.stateObj.attributes.current_humidity != null) {
|
||||||
return `${formatNumber(
|
return this.hass.formatEntityAttributeValue(
|
||||||
this.stateObj.attributes.current_humidity,
|
this.stateObj,
|
||||||
this.hass.locale
|
"current_humidity"
|
||||||
)}${blankBeforePercent(this.hass.locale)}%`;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -90,39 +88,33 @@ class HaClimateState extends LitElement {
|
|||||||
this.stateObj.attributes.target_temp_low != null &&
|
this.stateObj.attributes.target_temp_low != null &&
|
||||||
this.stateObj.attributes.target_temp_high != null
|
this.stateObj.attributes.target_temp_high != null
|
||||||
) {
|
) {
|
||||||
return `${formatNumber(
|
return `${this.hass.formatEntityAttributeValue(
|
||||||
this.stateObj.attributes.target_temp_low,
|
this.stateObj,
|
||||||
this.hass.locale
|
"target_temp_low"
|
||||||
)}-${formatNumber(
|
)}-${this.hass.formatEntityAttributeValue(
|
||||||
this.stateObj.attributes.target_temp_high,
|
this.stateObj,
|
||||||
this.hass.locale
|
"target_temp_high"
|
||||||
)} ${this.hass.config.unit_system.temperature}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.stateObj.attributes.temperature != null) {
|
if (this.stateObj.attributes.temperature != null) {
|
||||||
return `${formatNumber(
|
return this.hass.formatEntityAttributeValue(this.stateObj, "temperature");
|
||||||
this.stateObj.attributes.temperature,
|
|
||||||
this.hass.locale
|
|
||||||
)} ${this.hass.config.unit_system.temperature}`;
|
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
this.stateObj.attributes.target_humidity_low != null &&
|
this.stateObj.attributes.target_humidity_low != null &&
|
||||||
this.stateObj.attributes.target_humidity_high != null
|
this.stateObj.attributes.target_humidity_high != null
|
||||||
) {
|
) {
|
||||||
return `${formatNumber(
|
return `${this.hass.formatEntityAttributeValue(
|
||||||
this.stateObj.attributes.target_humidity_low,
|
this.stateObj,
|
||||||
this.hass.locale
|
"target_humidity_low"
|
||||||
)}-${formatNumber(
|
)}-${this.hass.formatEntityAttributeValue(
|
||||||
this.stateObj.attributes.target_humidity_high,
|
this.stateObj,
|
||||||
this.hass.locale
|
"target_humidity_high"
|
||||||
)} %`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.stateObj.attributes.humidity != null) {
|
if (this.stateObj.attributes.humidity != null) {
|
||||||
return `${formatNumber(
|
return this.hass.formatEntityAttributeValue(this.stateObj, "humidity");
|
||||||
this.stateObj.attributes.humidity,
|
|
||||||
this.hass.locale
|
|
||||||
)} %`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
|
@ -4,7 +4,7 @@ import type {
|
|||||||
CompletionResult,
|
CompletionResult,
|
||||||
CompletionSource,
|
CompletionSource,
|
||||||
} from "@codemirror/autocomplete";
|
} from "@codemirror/autocomplete";
|
||||||
import type { Extension } from "@codemirror/state";
|
import type { Extension, TransactionSpec } from "@codemirror/state";
|
||||||
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
||||||
import { HassEntities } from "home-assistant-js-websocket";
|
import { HassEntities } from "home-assistant-js-websocket";
|
||||||
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
|
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
|
||||||
@ -12,7 +12,7 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||||
import { loadCodeMirror } from "../resources/codemirror.ondemand";
|
import { CodeMirror, loadCodeMirror } from "../resources/codemirror.ondemand";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
|
|
||||||
@ -54,11 +54,11 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
@property({ type: Boolean, attribute: "autocomplete-icons" })
|
@property({ type: Boolean, attribute: "autocomplete-icons" })
|
||||||
public autocompleteIcons = false;
|
public autocompleteIcons = false;
|
||||||
|
|
||||||
@property() public error = false;
|
@property({ type: Boolean }) public error = false;
|
||||||
|
|
||||||
@state() private _value = "";
|
@state() private _value = "";
|
||||||
|
|
||||||
private _loadedCodeMirror?: typeof import("../resources/codemirror");
|
private _loadedCodeMirror?: CodeMirror;
|
||||||
|
|
||||||
private _iconList?: Completion[];
|
private _iconList?: Completion[];
|
||||||
|
|
||||||
@ -78,12 +78,19 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
this.codemirror.state,
|
this.codemirror.state,
|
||||||
[this._loadedCodeMirror.tags.comment]
|
[this._loadedCodeMirror.tags.comment]
|
||||||
);
|
);
|
||||||
return !!this.shadowRoot!.querySelector(`span.${className}`);
|
return !!this.renderRoot.querySelector(`span.${className}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
// Force update on reconnection so editor is recreated
|
||||||
|
if (this.hasUpdated) {
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
this.addEventListener("keydown", stopPropagation);
|
this.addEventListener("keydown", stopPropagation);
|
||||||
|
// This is unreachable as editor will not exist yet,
|
||||||
|
// but focus should not behave like this for good a11y.
|
||||||
|
// (@steverep to fix in autofocus PR)
|
||||||
if (!this.codemirror) {
|
if (!this.codemirror) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -95,31 +102,41 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.removeEventListener("keydown", stopPropagation);
|
this.removeEventListener("keydown", stopPropagation);
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
this.codemirror!.destroy();
|
||||||
|
delete this.codemirror;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure CodeMirror module is loaded before any update
|
||||||
|
protected override async scheduleUpdate() {
|
||||||
|
this._loadedCodeMirror ??= await loadCodeMirror();
|
||||||
|
super.scheduleUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected update(changedProps: PropertyValues): void {
|
protected update(changedProps: PropertyValues): void {
|
||||||
super.update(changedProps);
|
super.update(changedProps);
|
||||||
|
|
||||||
if (!this.codemirror) {
|
if (!this.codemirror) {
|
||||||
|
this._createCodeMirror();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const transactions: TransactionSpec[] = [];
|
||||||
if (changedProps.has("mode")) {
|
if (changedProps.has("mode")) {
|
||||||
this.codemirror.dispatch({
|
transactions.push({
|
||||||
effects: this._loadedCodeMirror!.langCompartment!.reconfigure(
|
effects: this._loadedCodeMirror!.langCompartment!.reconfigure(
|
||||||
this._mode
|
this._mode
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (changedProps.has("readOnly")) {
|
if (changedProps.has("readOnly")) {
|
||||||
this.codemirror.dispatch({
|
transactions.push({
|
||||||
effects: this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
|
effects: this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
|
||||||
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
|
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (changedProps.has("_value") && this._value !== this.value) {
|
if (changedProps.has("_value") && this._value !== this.value) {
|
||||||
this.codemirror.dispatch({
|
transactions.push({
|
||||||
changes: {
|
changes: {
|
||||||
from: 0,
|
from: 0,
|
||||||
to: this.codemirror.state.doc.length,
|
to: this.codemirror.state.doc.length,
|
||||||
@ -127,46 +144,45 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (transactions.length > 0) {
|
||||||
|
this.codemirror.dispatch(...transactions);
|
||||||
|
}
|
||||||
if (changedProps.has("error")) {
|
if (changedProps.has("error")) {
|
||||||
this.classList.toggle("error-state", this.error);
|
this.classList.toggle("error-state", this.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues): void {
|
|
||||||
super.firstUpdated(changedProps);
|
|
||||||
this._load();
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _mode() {
|
private get _mode() {
|
||||||
return this._loadedCodeMirror!.langs[this.mode];
|
return this._loadedCodeMirror!.langs[this.mode];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _load(): Promise<void> {
|
private _createCodeMirror() {
|
||||||
this._loadedCodeMirror = await loadCodeMirror();
|
if (!this._loadedCodeMirror) {
|
||||||
|
throw new Error("Cannot create editor before CodeMirror is loaded");
|
||||||
|
}
|
||||||
const extensions: Extension[] = [
|
const extensions: Extension[] = [
|
||||||
this._loadedCodeMirror.lineNumbers(),
|
this._loadedCodeMirror.lineNumbers(),
|
||||||
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
|
|
||||||
this._loadedCodeMirror.history(),
|
this._loadedCodeMirror.history(),
|
||||||
|
this._loadedCodeMirror.drawSelection(),
|
||||||
|
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
|
||||||
|
this._loadedCodeMirror.rectangularSelection(),
|
||||||
|
this._loadedCodeMirror.crosshairCursor(),
|
||||||
this._loadedCodeMirror.highlightSelectionMatches(),
|
this._loadedCodeMirror.highlightSelectionMatches(),
|
||||||
this._loadedCodeMirror.highlightActiveLine(),
|
this._loadedCodeMirror.highlightActiveLine(),
|
||||||
this._loadedCodeMirror.drawSelection(),
|
|
||||||
this._loadedCodeMirror.rectangularSelection(),
|
|
||||||
this._loadedCodeMirror.keymap.of([
|
this._loadedCodeMirror.keymap.of([
|
||||||
...this._loadedCodeMirror.defaultKeymap,
|
...this._loadedCodeMirror.defaultKeymap,
|
||||||
...this._loadedCodeMirror.searchKeymap,
|
...this._loadedCodeMirror.searchKeymap,
|
||||||
...this._loadedCodeMirror.historyKeymap,
|
...this._loadedCodeMirror.historyKeymap,
|
||||||
...this._loadedCodeMirror.tabKeyBindings,
|
...this._loadedCodeMirror.tabKeyBindings,
|
||||||
saveKeyBinding,
|
saveKeyBinding,
|
||||||
] as KeyBinding[]),
|
]),
|
||||||
this._loadedCodeMirror.langCompartment.of(this._mode),
|
this._loadedCodeMirror.langCompartment.of(this._mode),
|
||||||
this._loadedCodeMirror.haTheme,
|
this._loadedCodeMirror.haTheme,
|
||||||
this._loadedCodeMirror.haSyntaxHighlighting,
|
this._loadedCodeMirror.haSyntaxHighlighting,
|
||||||
this._loadedCodeMirror.readonlyCompartment.of(
|
this._loadedCodeMirror.readonlyCompartment.of(
|
||||||
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
|
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
|
||||||
),
|
),
|
||||||
this._loadedCodeMirror.EditorView.updateListener.of((update) =>
|
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
|
||||||
this._onUpdate(update)
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!this.readOnly) {
|
if (!this.readOnly) {
|
||||||
@ -192,8 +208,7 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
doc: this._value,
|
doc: this._value,
|
||||||
extensions,
|
extensions,
|
||||||
}),
|
}),
|
||||||
root: this.shadowRoot!,
|
parent: this.renderRoot,
|
||||||
parent: this.shadowRoot!,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,17 +292,13 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onUpdate(update: ViewUpdate): void {
|
private _onUpdate = (update: ViewUpdate): void => {
|
||||||
if (!update.docChanged) {
|
if (!update.docChanged) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newValue = this.value;
|
this._value = update.state.doc.toString();
|
||||||
if (newValue === this._value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._value = newValue;
|
|
||||||
fireEvent(this, "value-changed", { value: this._value });
|
fireEvent(this, "value-changed", { value: this._value });
|
||||||
}
|
};
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
|
@ -312,6 +312,10 @@ export class HaComboBox extends LitElement {
|
|||||||
|
|
||||||
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
|
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
if (!this.allowCustomValue) {
|
||||||
|
// @ts-ignore
|
||||||
|
this._comboBox._closeOnBlurIsPrevented = true;
|
||||||
|
}
|
||||||
const newValue = ev.detail.value;
|
const newValue = ev.detail.value;
|
||||||
|
|
||||||
if (newValue !== this.value) {
|
if (newValue !== this.value) {
|
||||||
|
@ -18,10 +18,9 @@ import {
|
|||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { clamp } from "../common/number/clamp";
|
import { clamp } from "../common/number/clamp";
|
||||||
import { arc } from "../resources/svg-arc";
|
import { svgArc } from "../resources/svg-arc";
|
||||||
|
|
||||||
const MAX_ANGLE = 270;
|
const MAX_ANGLE = 270;
|
||||||
const ROTATE_ANGLE = 360 - MAX_ANGLE / 2 - 90;
|
const ROTATE_ANGLE = 360 - MAX_ANGLE / 2 - 90;
|
||||||
@ -60,6 +59,8 @@ const A11Y_KEY_CODES = new Set([
|
|||||||
"End",
|
"End",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export type ControlCircularSliderMode = "start" | "end" | "full";
|
||||||
|
|
||||||
@customElement("ha-control-circular-slider")
|
@customElement("ha-control-circular-slider")
|
||||||
export class HaControlCircularSlider extends LitElement {
|
export class HaControlCircularSlider extends LitElement {
|
||||||
@property({ type: Boolean, reflect: true })
|
@property({ type: Boolean, reflect: true })
|
||||||
@ -68,8 +69,11 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
public dual?: boolean;
|
public dual?: boolean;
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true })
|
@property({ type: String })
|
||||||
public inverted?: boolean;
|
public mode?: ControlCircularSliderMode;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public inactive?: boolean;
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
public label?: string;
|
public label?: string;
|
||||||
@ -388,44 +392,151 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _strokeDashArc(
|
private _strokeCircleDashArc(value: number): [string, string] {
|
||||||
percentage: number,
|
return this._strokeDashArc(value, value);
|
||||||
inverted?: boolean
|
}
|
||||||
): [string, string] {
|
|
||||||
const maxRatio = MAX_ANGLE / 360;
|
private _strokeDashArc(from: number, to: number): [string, string] {
|
||||||
const f = RADIUS * 2 * Math.PI;
|
const start = this._valueToPercentage(from);
|
||||||
if (inverted) {
|
const end = this._valueToPercentage(to);
|
||||||
const arcLength = (1 - percentage) * f * maxRatio;
|
|
||||||
const strokeDasharray = `${arcLength} ${f - arcLength}`;
|
const track = (RADIUS * 2 * Math.PI * MAX_ANGLE) / 360;
|
||||||
const strokeDashOffset = `${arcLength + f * (1 - maxRatio)}`;
|
const arc = Math.max((end - start) * track, 0);
|
||||||
return [strokeDasharray, strokeDashOffset];
|
const arcOffset = start * track - 0.5;
|
||||||
}
|
|
||||||
const arcLength = percentage * f * maxRatio;
|
const strokeDasharray = `${arc} ${track - arc}`;
|
||||||
const strokeDasharray = `${arcLength} ${f - arcLength}`;
|
const strokeDashOffset = `-${arcOffset}`;
|
||||||
const strokeDashOffset = "0";
|
|
||||||
return [strokeDasharray, strokeDashOffset];
|
return [strokeDasharray, strokeDashOffset];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected renderArc(
|
||||||
|
id: string,
|
||||||
|
value: number | undefined,
|
||||||
|
mode: ControlCircularSliderMode
|
||||||
|
) {
|
||||||
|
if (this.disabled) return nothing;
|
||||||
|
|
||||||
|
const path = svgArc({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
start: 0,
|
||||||
|
end: MAX_ANGLE,
|
||||||
|
r: RADIUS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const limit = mode === "end" ? this.max : this.min;
|
||||||
|
|
||||||
|
const current = this.current ?? limit;
|
||||||
|
const target = value ?? limit;
|
||||||
|
|
||||||
|
const showActive =
|
||||||
|
mode === "end"
|
||||||
|
? target <= current
|
||||||
|
: mode === "start"
|
||||||
|
? current <= target
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const activeArc = showActive
|
||||||
|
? mode === "end"
|
||||||
|
? this._strokeDashArc(target, current)
|
||||||
|
: this._strokeDashArc(current, target)
|
||||||
|
: this._strokeCircleDashArc(target);
|
||||||
|
|
||||||
|
const coloredArc =
|
||||||
|
mode === "full"
|
||||||
|
? this._strokeDashArc(this.min, this.max)
|
||||||
|
: mode === "end"
|
||||||
|
? this._strokeDashArc(target, limit)
|
||||||
|
: this._strokeDashArc(limit, target);
|
||||||
|
|
||||||
|
const targetCircle = this._strokeCircleDashArc(target);
|
||||||
|
|
||||||
|
const currentCircle =
|
||||||
|
this.current != null &&
|
||||||
|
this.current <= this.max &&
|
||||||
|
this.current >= this.min &&
|
||||||
|
(showActive || this.mode === "full")
|
||||||
|
? this._strokeCircleDashArc(this.current)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return svg`
|
||||||
|
<g class=${classMap({ inactive: Boolean(this.inactive) })}>
|
||||||
|
<path
|
||||||
|
class="arc arc-clear"
|
||||||
|
d=${path}
|
||||||
|
stroke-dasharray=${coloredArc[0]}
|
||||||
|
stroke-dashoffset=${coloredArc[1]}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="arc arc-colored ${classMap({ [id]: true })}"
|
||||||
|
d=${path}
|
||||||
|
stroke-dasharray=${coloredArc[0]}
|
||||||
|
stroke-dashoffset=${coloredArc[1]}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
.id=${id}
|
||||||
|
d=${path}
|
||||||
|
class="arc arc-active ${classMap({ [id]: true })}"
|
||||||
|
stroke-dasharray=${activeArc[0]}
|
||||||
|
stroke-dashoffset=${activeArc[1]}
|
||||||
|
role="slider"
|
||||||
|
tabindex="0"
|
||||||
|
aria-valuemin=${this.min}
|
||||||
|
aria-valuemax=${this.max}
|
||||||
|
aria-valuenow=${
|
||||||
|
this._localValue != null
|
||||||
|
? this._steppedValue(this._localValue)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
aria-disabled=${this.disabled}
|
||||||
|
aria-label=${ifDefined(this.lowLabel ?? this.label)}
|
||||||
|
@keydown=${this._handleKeyDown}
|
||||||
|
@keyup=${this._handleKeyUp}
|
||||||
|
/>
|
||||||
|
${
|
||||||
|
currentCircle
|
||||||
|
? svg`
|
||||||
|
<path
|
||||||
|
class="current arc-current"
|
||||||
|
d=${path}
|
||||||
|
stroke-dasharray=${currentCircle[0]}
|
||||||
|
stroke-dashoffset=${currentCircle[1]}
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
<path
|
||||||
|
class="target-border ${classMap({ [id]: true })}"
|
||||||
|
d=${path}
|
||||||
|
stroke-dasharray=${targetCircle[0]}
|
||||||
|
stroke-dashoffset=${targetCircle[1]}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="target"
|
||||||
|
d=${path}
|
||||||
|
stroke-dasharray=${targetCircle[0]}
|
||||||
|
stroke-dashoffset=${targetCircle[1]}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const trackPath = arc({ x: 0, y: 0, start: 0, end: MAX_ANGLE, r: RADIUS });
|
const trackPath = svgArc({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
start: 0,
|
||||||
|
end: MAX_ANGLE,
|
||||||
|
r: RADIUS,
|
||||||
|
});
|
||||||
|
|
||||||
const lowValue = this.dual ? this._localLow : this._localValue;
|
const lowValue = this.dual ? this._localLow : this._localValue;
|
||||||
const highValue = this._localHigh;
|
const highValue = this._localHigh;
|
||||||
const lowPercentage = this._valueToPercentage(lowValue ?? this.min);
|
const current = this.current;
|
||||||
const highPercentage = this._valueToPercentage(highValue ?? this.max);
|
|
||||||
|
|
||||||
const [lowStrokeDasharray, lowStrokeDashOffset] = this._strokeDashArc(
|
const currentStroke = current
|
||||||
lowPercentage,
|
? this._strokeCircleDashArc(current)
|
||||||
this.inverted
|
: undefined;
|
||||||
);
|
|
||||||
|
|
||||||
const [highStrokeDasharray, highStrokeDashOffset] = this._strokeDashArc(
|
|
||||||
highPercentage,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentPercentage = this._valueToPercentage(this.current ?? 0);
|
|
||||||
const currentAngle = currentPercentage * MAX_ANGLE;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<svg
|
<svg
|
||||||
@ -447,79 +558,25 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
</g>
|
</g>
|
||||||
<g id="display">
|
<g id="display">
|
||||||
<path class="background" d=${trackPath} />
|
<path class="background" d=${trackPath} />
|
||||||
${lowValue != null
|
${currentStroke
|
||||||
? svg`
|
? svg`
|
||||||
<circle
|
<path
|
||||||
.id=${this.dual ? "low" : "value"}
|
class="current"
|
||||||
class="track"
|
d=${trackPath}
|
||||||
cx="0"
|
stroke-dasharray=${currentStroke[0]}
|
||||||
cy="0"
|
stroke-dashoffset=${currentStroke[1]}
|
||||||
r=${RADIUS}
|
/>
|
||||||
stroke-dasharray=${lowStrokeDasharray}
|
`
|
||||||
stroke-dashoffset=${lowStrokeDashOffset}
|
: nothing}
|
||||||
role="slider"
|
${lowValue != null
|
||||||
tabindex="0"
|
? this.renderArc(
|
||||||
aria-valuemin=${this.min}
|
this.dual ? "low" : "value",
|
||||||
aria-valuemax=${this.max}
|
lowValue,
|
||||||
aria-valuenow=${
|
(!this.dual && this.mode) || "start"
|
||||||
lowValue != null ? this._steppedValue(lowValue) : undefined
|
)
|
||||||
}
|
|
||||||
aria-disabled=${this.disabled}
|
|
||||||
aria-label=${ifDefined(this.lowLabel ?? this.label)}
|
|
||||||
@keydown=${this._handleKeyDown}
|
|
||||||
@keyup=${this._handleKeyUp}
|
|
||||||
/>
|
|
||||||
`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
${this.dual && highValue != null
|
${this.dual && highValue != null
|
||||||
? svg`
|
? this.renderArc("high", highValue, "end")
|
||||||
<circle
|
|
||||||
id="high"
|
|
||||||
class="track"
|
|
||||||
cx="0"
|
|
||||||
cy="0"
|
|
||||||
r=${RADIUS}
|
|
||||||
stroke-dasharray=${highStrokeDasharray}
|
|
||||||
stroke-dashoffset=${highStrokeDashOffset}
|
|
||||||
role="slider"
|
|
||||||
tabindex="0"
|
|
||||||
aria-valuemin=${this.min}
|
|
||||||
aria-valuemax=${this.max}
|
|
||||||
aria-valuenow=${
|
|
||||||
highValue != null
|
|
||||||
? this._steppedValue(highValue)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
aria-disabled=${this.disabled}
|
|
||||||
aria-label=${ifDefined(this.highLabel)}
|
|
||||||
@keydown=${this._handleKeyDown}
|
|
||||||
@keyup=${this._handleKeyUp}
|
|
||||||
/>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${this.current != null
|
|
||||||
? svg`
|
|
||||||
<g
|
|
||||||
style=${styleMap({ "--current-angle": `${currentAngle}deg` })}
|
|
||||||
class="current"
|
|
||||||
>
|
|
||||||
<line
|
|
||||||
x1=${RADIUS - 12}
|
|
||||||
y1="0"
|
|
||||||
x2=${RADIUS - 15}
|
|
||||||
y2="0"
|
|
||||||
stroke-width="4"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1=${RADIUS - 15}
|
|
||||||
y1="0"
|
|
||||||
x2=${RADIUS - 20}
|
|
||||||
y2="0"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-width="4"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
@ -531,7 +588,7 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
--control-circular-slider-color: var(--primary-color);
|
--control-circular-slider-color: var(--primary-color);
|
||||||
--control-circular-slider-background: #8b97a3;
|
--control-circular-slider-background: var(--disabled-color);
|
||||||
--control-circular-slider-background-opacity: 0.3;
|
--control-circular-slider-background-opacity: 0.3;
|
||||||
--control-circular-slider-low-color: var(
|
--control-circular-slider-low-color: var(
|
||||||
--control-circular-slider-color
|
--control-circular-slider-color
|
||||||
@ -573,8 +630,7 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
stroke-width: 24px;
|
stroke-width: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track {
|
.arc {
|
||||||
outline: none;
|
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-width: 24px;
|
stroke-width: 24px;
|
||||||
@ -586,29 +642,87 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
opacity 180ms ease-in-out;
|
opacity 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track:focus-visible {
|
.target {
|
||||||
stroke-width: 28px;
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width: 18px;
|
||||||
|
stroke: white;
|
||||||
|
transition:
|
||||||
|
stroke-width 300ms ease-in-out,
|
||||||
|
stroke-dasharray 300ms ease-in-out,
|
||||||
|
stroke-dashoffset 300ms ease-in-out,
|
||||||
|
stroke 180ms ease-in-out,
|
||||||
|
opacity 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pressed .track {
|
.target-border {
|
||||||
transition: stroke-width 300ms ease-in-out;
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width: 24px;
|
||||||
|
stroke: white;
|
||||||
|
transition:
|
||||||
|
stroke-width 300ms ease-in-out,
|
||||||
|
stroke-dasharray 300ms ease-in-out,
|
||||||
|
stroke-dashoffset 300ms ease-in-out,
|
||||||
|
stroke 180ms ease-in-out,
|
||||||
|
opacity 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current {
|
.current {
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width: 8px;
|
||||||
stroke: var(--primary-text-color);
|
stroke: var(--primary-text-color);
|
||||||
transform: rotate(var(--current-angle, 0));
|
opacity: 0.5;
|
||||||
transition: transform 300ms ease-in-out;
|
transition:
|
||||||
|
stroke-width 300ms ease-in-out,
|
||||||
|
stroke-dasharray 300ms ease-in-out,
|
||||||
|
stroke-dashoffset 300ms ease-in-out,
|
||||||
|
stroke 180ms ease-in-out,
|
||||||
|
opacity 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#value {
|
.arc-current {
|
||||||
|
stroke: var(--clear-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arc-clear {
|
||||||
|
stroke: var(--clear-background-color);
|
||||||
|
}
|
||||||
|
.arc-colored {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.arc-active {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.arc-active:focus-visible {
|
||||||
|
stroke-width: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pressed .arc,
|
||||||
|
.pressed .target,
|
||||||
|
.pressed .target-border,
|
||||||
|
.pressed .current {
|
||||||
|
transition:
|
||||||
|
stroke-width 300ms ease-in-out,
|
||||||
|
stroke 180ms ease-in-out,
|
||||||
|
opacity 180ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive .arc,
|
||||||
|
.inactive .arc-current {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
stroke: var(--control-circular-slider-color);
|
stroke: var(--control-circular-slider-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#low {
|
.low {
|
||||||
stroke: var(--control-circular-slider-low-color);
|
stroke: var(--control-circular-slider-low-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#high {
|
.high {
|
||||||
stroke: var(--control-circular-slider-high-color);
|
stroke: var(--control-circular-slider-high-color);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
258
src/components/ha-control-number-buttons.ts
Normal file
258
src/components/ha-control-number-buttons.ts
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
import { mdiMinus, mdiPlus } from "@mdi/js";
|
||||||
|
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||||
|
import { customElement, property, query } from "lit/decorators";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
import { conditionalClamp } from "../common/number/clamp";
|
||||||
|
import { formatNumber } from "../common/number/format_number";
|
||||||
|
import { FrontendLocaleData } from "../data/translation";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
|
||||||
|
const A11Y_KEY_CODES = new Set([
|
||||||
|
"ArrowRight",
|
||||||
|
"ArrowUp",
|
||||||
|
"ArrowLeft",
|
||||||
|
"ArrowDown",
|
||||||
|
"PageUp",
|
||||||
|
"PageDown",
|
||||||
|
"Home",
|
||||||
|
"End",
|
||||||
|
]);
|
||||||
|
|
||||||
|
@customElement("ha-control-number-buttons")
|
||||||
|
export class HaControlNumberButton extends LitElement {
|
||||||
|
@property({ attribute: false }) public locale?: FrontendLocaleData;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ type: Number }) public step?: number;
|
||||||
|
|
||||||
|
@property({ type: Number }) public value?: number;
|
||||||
|
|
||||||
|
@property({ type: Number }) public min?: number;
|
||||||
|
|
||||||
|
@property({ type: Number }) public max?: number;
|
||||||
|
|
||||||
|
@property({ attribute: "false" })
|
||||||
|
public formatOptions: Intl.NumberFormatOptions = {};
|
||||||
|
|
||||||
|
@query("#input") _input!: HTMLDivElement;
|
||||||
|
|
||||||
|
private boundedValue(value: number) {
|
||||||
|
const clamped = conditionalClamp(value, this.min, this.max);
|
||||||
|
return Math.round(clamped / this._step) * this._step;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _step() {
|
||||||
|
return this.step ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _tenPercentStep() {
|
||||||
|
if (this.max == null || this.min == null) return this._step;
|
||||||
|
const range = this.max - this.min / 10;
|
||||||
|
|
||||||
|
if (range <= this._step) return this._step;
|
||||||
|
return Math.max(range / 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handlePlusButton() {
|
||||||
|
this._increment();
|
||||||
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
this._input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleMinusButton() {
|
||||||
|
this._decrement();
|
||||||
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
this._input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _increment() {
|
||||||
|
this.value = this.boundedValue(this._value + this._step);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _decrement() {
|
||||||
|
this.value = this.boundedValue(this._value - this._step);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
switch (e.code) {
|
||||||
|
case "ArrowRight":
|
||||||
|
case "ArrowUp":
|
||||||
|
this._increment();
|
||||||
|
break;
|
||||||
|
case "ArrowLeft":
|
||||||
|
case "ArrowDown":
|
||||||
|
this._decrement();
|
||||||
|
break;
|
||||||
|
case "PageUp":
|
||||||
|
this.value = this.boundedValue(this._value + this._tenPercentStep);
|
||||||
|
break;
|
||||||
|
case "PageDown":
|
||||||
|
this.value = this.boundedValue(this._value - this._tenPercentStep);
|
||||||
|
break;
|
||||||
|
case "Home":
|
||||||
|
if (this.min != null) {
|
||||||
|
this.value = this.min;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "End":
|
||||||
|
if (this.max != null) {
|
||||||
|
this.value = this.max;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const displayedValue =
|
||||||
|
this.value != null
|
||||||
|
? formatNumber(this.value, this.locale, this.formatOptions)
|
||||||
|
: "-";
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<div
|
||||||
|
id="input"
|
||||||
|
class="value"
|
||||||
|
role="number-button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-valuenow=${this.value}
|
||||||
|
aria-valuemin=${this.min}
|
||||||
|
aria-valuemax=${this.max}
|
||||||
|
aria-label=${ifDefined(this.label)}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
@keydown=${this._handleKeyDown}
|
||||||
|
>
|
||||||
|
${displayedValue}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="button minus"
|
||||||
|
type="button"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-label="decrement"
|
||||||
|
@click=${this._handleMinusButton}
|
||||||
|
.disabled=${this.disabled ||
|
||||||
|
(this.min != null && this._value <= this.min)}
|
||||||
|
>
|
||||||
|
<ha-svg-icon aria-hidden .path=${mdiMinus}></ha-svg-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button plus"
|
||||||
|
type="button"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-label="increment"
|
||||||
|
@click=${this._handlePlusButton}
|
||||||
|
.disabled=${this.disabled ||
|
||||||
|
(this.max != null && this._value >= this.max)}
|
||||||
|
>
|
||||||
|
<ha-svg-icon aria-hidden .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
--control-number-buttons-focus-color: var(--primary-color);
|
||||||
|
--control-number-buttons-background-color: var(--disabled-color);
|
||||||
|
--control-number-buttons-background-opacity: 0.2;
|
||||||
|
--control-number-buttons-border-radius: 10px;
|
||||||
|
--mdc-icon-size: 16px;
|
||||||
|
height: 40px;
|
||||||
|
width: 200px;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 180ms ease-in-out;
|
||||||
|
}
|
||||||
|
:host([disabled]) {
|
||||||
|
color: var(--disabled-color);
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 44px;
|
||||||
|
border-radius: var(--control-number-buttons-border-radius);
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
line-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
/* For safari border-radius overflow */
|
||||||
|
z-index: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.value::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--control-number-buttons-background-color);
|
||||||
|
transition:
|
||||||
|
background-color 180ms ease-in-out,
|
||||||
|
opacity 180ms ease-in-out;
|
||||||
|
opacity: var(--control-number-buttons-background-opacity);
|
||||||
|
}
|
||||||
|
.value:focus-visible {
|
||||||
|
box-shadow: 0 0 0 2px var(--control-number-buttons-focus-color);
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
color: inherit;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 35px;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.button[disabled] {
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.button.minus {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.button.plus {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-control-number-buttons": HaControlNumberButton;
|
||||||
|
}
|
||||||
|
}
|
270
src/components/ha-control-select-menu.ts
Normal file
270
src/components/ha-control-select-menu.ts
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
import { Ripple } from "@material/mwc-ripple";
|
||||||
|
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||||
|
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||||
|
import { css, html, nothing } from "lit";
|
||||||
|
import {
|
||||||
|
customElement,
|
||||||
|
eventOptions,
|
||||||
|
query,
|
||||||
|
queryAsync,
|
||||||
|
state,
|
||||||
|
} from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
import { debounce } from "../common/util/debounce";
|
||||||
|
import { nextRender } from "../common/util/render-status";
|
||||||
|
import "./ha-icon";
|
||||||
|
import type { HaIcon } from "./ha-icon";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
import type { HaSvgIcon } from "./ha-svg-icon";
|
||||||
|
|
||||||
|
@customElement("ha-control-select-menu")
|
||||||
|
export class HaControlSelectMenu extends SelectBase {
|
||||||
|
@query(".select") protected mdcRoot!: HTMLElement;
|
||||||
|
|
||||||
|
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
|
||||||
|
|
||||||
|
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
||||||
|
|
||||||
|
@state() private _shouldRenderRipple = false;
|
||||||
|
|
||||||
|
public override render() {
|
||||||
|
const classes = {
|
||||||
|
"select-disabled": this.disabled,
|
||||||
|
"select-required": this.required,
|
||||||
|
"select-invalid": !this.isUiValid,
|
||||||
|
"select-no-value": !this.selectedText,
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelledby = this.label ? "label" : undefined;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="select ${classMap(classes)}">
|
||||||
|
<input
|
||||||
|
class="formElement"
|
||||||
|
.name=${this.name}
|
||||||
|
.value=${this.value}
|
||||||
|
hidden
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
?required=${this.required}
|
||||||
|
/>
|
||||||
|
<!-- @ts-ignore -->
|
||||||
|
<div
|
||||||
|
class="select-anchor"
|
||||||
|
aria-autocomplete="none"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded=${this.menuOpen}
|
||||||
|
aria-invalid=${!this.isUiValid}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-labelledby=${ifDefined(labelledby)}
|
||||||
|
aria-required=${this.required}
|
||||||
|
@click=${this.onClick}
|
||||||
|
@focus=${this.onFocus}
|
||||||
|
@blur=${this.onBlur}
|
||||||
|
@keydown=${this.onKeydown}
|
||||||
|
@mousedown=${this.handleRippleActivate}
|
||||||
|
@mouseup=${this.handleRippleDeactivate}
|
||||||
|
@mouseenter=${this.handleRippleMouseEnter}
|
||||||
|
@mouseleave=${this.handleRippleMouseLeave}
|
||||||
|
@touchstart=${this.handleRippleActivate}
|
||||||
|
@touchend=${this.handleRippleDeactivate}
|
||||||
|
@touchcancel=${this.handleRippleDeactivate}
|
||||||
|
>
|
||||||
|
${this.renderIcon()}
|
||||||
|
<div class="content">
|
||||||
|
<p id="label" class="label">${this.label}</p>
|
||||||
|
${this.selectedText
|
||||||
|
? html`<p class="value">${this.selectedText}</p>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._shouldRenderRipple && !this.disabled
|
||||||
|
? html` <mwc-ripple></mwc-ripple> `
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this.renderMenu()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderIcon() {
|
||||||
|
const index = this.mdcFoundation?.getSelectedIndex();
|
||||||
|
const items = this.menuElement?.items ?? [];
|
||||||
|
const item = index != null ? items[index] : undefined;
|
||||||
|
const icon =
|
||||||
|
item?.querySelector("[slot='graphic']") ??
|
||||||
|
(null as HaSvgIcon | HaIcon | null);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="icon">
|
||||||
|
${icon && "path" in icon
|
||||||
|
? html`<ha-svg-icon .path=${icon.path}></ha-svg-icon>`
|
||||||
|
: icon && "icon" in icon
|
||||||
|
? html`<ha-icon .path=${icon.icon}></ha-icon>`
|
||||||
|
: html`<slot name="icon"></slot>`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onFocus() {
|
||||||
|
this.handleRippleFocus();
|
||||||
|
super.onFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onBlur() {
|
||||||
|
this.handleRippleBlur();
|
||||||
|
super.onBlur();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
|
||||||
|
this._shouldRenderRipple = true;
|
||||||
|
return this._ripple;
|
||||||
|
});
|
||||||
|
|
||||||
|
@eventOptions({ passive: true })
|
||||||
|
private handleRippleActivate(evt?: Event) {
|
||||||
|
this._rippleHandlers.startPress(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleDeactivate() {
|
||||||
|
this._rippleHandlers.endPress();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleMouseEnter() {
|
||||||
|
this._rippleHandlers.startHover();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleMouseLeave() {
|
||||||
|
this._rippleHandlers.endHover();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleFocus() {
|
||||||
|
this._rippleHandlers.startFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleBlur() {
|
||||||
|
this._rippleHandlers.endFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
window.addEventListener("translations-updated", this._translationsUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
window.removeEventListener(
|
||||||
|
"translations-updated",
|
||||||
|
this._translationsUpdated
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _translationsUpdated = debounce(async () => {
|
||||||
|
await nextRender();
|
||||||
|
this.layoutOptions();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
static override styles = [
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
--control-select-menu-text-color: var(--primary-text-color);
|
||||||
|
--control-select-menu-background-color: var(--disabled-color);
|
||||||
|
--control-select-menu-background-opacity: 0.2;
|
||||||
|
--control-select-menu-border-radius: 14px;
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
width: auto;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.select-anchor {
|
||||||
|
height: 48px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: var(--control-select-menu-border-radius);
|
||||||
|
box-sizing: border-box;
|
||||||
|
outline: none;
|
||||||
|
overflow: hidden;
|
||||||
|
background: none;
|
||||||
|
--mdc-ripple-color: var(--control-select-menu-background-color);
|
||||||
|
/* For safari border-radius overflow */
|
||||||
|
z-index: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
transition: color 180ms ease-in-out;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: 0.25px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content p {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-no-value .label {
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-anchor::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--control-select-menu-background-color);
|
||||||
|
transition:
|
||||||
|
background-color 180ms ease-in-out,
|
||||||
|
opacity 180ms ease-in-out;
|
||||||
|
opacity: var(--control-select-menu-background-opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-disabled .select-anchor {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--disabled-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-menu {
|
||||||
|
--mdc-shape-medium: 8px;
|
||||||
|
}
|
||||||
|
mwc-list {
|
||||||
|
--mdc-list-vertical-padding: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-control-select-menu": HaControlSelectMenu;
|
||||||
|
}
|
||||||
|
}
|
@ -43,6 +43,9 @@ export class HaControlSlider extends LitElement {
|
|||||||
@property({ type: Boolean, attribute: "show-handle" })
|
@property({ type: Boolean, attribute: "show-handle" })
|
||||||
public showHandle = false;
|
public showHandle = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "inverted" })
|
||||||
|
public inverted = false;
|
||||||
|
|
||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
public value?: number;
|
public value?: number;
|
||||||
|
|
||||||
@ -61,11 +64,16 @@ export class HaControlSlider extends LitElement {
|
|||||||
public pressed = false;
|
public pressed = false;
|
||||||
|
|
||||||
valueToPercentage(value: number) {
|
valueToPercentage(value: number) {
|
||||||
return (this.boundedValue(value) - this.min) / (this.max - this.min);
|
const percentage =
|
||||||
|
(this.boundedValue(value) - this.min) / (this.max - this.min);
|
||||||
|
return this.inverted ? 1 - percentage : percentage;
|
||||||
}
|
}
|
||||||
|
|
||||||
percentageToValue(value: number) {
|
percentageToValue(percentage: number) {
|
||||||
return (this.max - this.min) * value + this.min;
|
return (
|
||||||
|
(this.max - this.min) * (this.inverted ? 1 - percentage : percentage) +
|
||||||
|
this.min
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
steppedValue(value: number) {
|
steppedValue(value: number) {
|
||||||
|
@ -10,12 +10,12 @@ import "./ha-icon-button";
|
|||||||
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
|
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
|
||||||
|
|
||||||
export const createCloseHeading = (
|
export const createCloseHeading = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant | undefined,
|
||||||
title: string | TemplateResult
|
title: string | TemplateResult
|
||||||
) => html`
|
) => html`
|
||||||
<div class="header_title">${title}</div>
|
<div class="header_title">${title}</div>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.label=${hass.localize("ui.dialogs.generic.close")}
|
.label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
|
||||||
.path=${mdiClose}
|
.path=${mdiClose}
|
||||||
dialogAction="close"
|
dialogAction="close"
|
||||||
class="header_button"
|
class="header_button"
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
|
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||||
import { mdiClose } from "@mdi/js";
|
import { mdiDelete, mdiFileUpload } from "@mdi/js";
|
||||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
import { LitElement, PropertyValues, TemplateResult, css, html } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./ha-circular-progress";
|
import "./ha-button";
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
|
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||||
|
import { ensureArray } from "../common/array/ensure-array";
|
||||||
|
import { bytesToString } from "../util/bytes-to-string";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"file-picked": { files: FileList };
|
"file-picked": { files: File[] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,12 +25,22 @@ export class HaFileUpload extends LitElement {
|
|||||||
|
|
||||||
@property() public icon?: string;
|
@property() public icon?: string;
|
||||||
|
|
||||||
@property() public label!: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public value: string | TemplateResult | null = null;
|
@property() public secondary?: string;
|
||||||
|
|
||||||
|
@property() public supports?: string;
|
||||||
|
|
||||||
|
@property() public value?: File | File[] | FileList | string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) private multiple = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public disabled: boolean = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) private uploading = false;
|
@property({ type: Boolean }) private uploading = false;
|
||||||
|
|
||||||
|
@property({ type: Number }) private progress?: number;
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "auto-open-file-dialog" })
|
@property({ type: Boolean, attribute: "auto-open-file-dialog" })
|
||||||
private autoOpenFileDialog = false;
|
private autoOpenFileDialog = false;
|
||||||
|
|
||||||
@ -45,72 +58,102 @@ export class HaFileUpload extends LitElement {
|
|||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
${this.uploading
|
${this.uploading
|
||||||
? html`<ha-circular-progress
|
? html`<div class="container">
|
||||||
alt="Uploading"
|
<div class="row">
|
||||||
size="large"
|
<span class="header"
|
||||||
active
|
>${this.value
|
||||||
></ha-circular-progress>`
|
? this.hass?.localize(
|
||||||
: html`
|
"ui.components.file-upload.uploading_name",
|
||||||
<label
|
{ name: this.value }
|
||||||
for="input"
|
)
|
||||||
class="mdc-text-field mdc-text-field--filled ${classMap({
|
: this.hass?.localize(
|
||||||
"mdc-text-field--focused": this._drag,
|
"ui.components.file-upload.uploading"
|
||||||
"mdc-text-field--with-leading-icon": Boolean(this.icon),
|
)}</span
|
||||||
"mdc-text-field--with-trailing-icon": Boolean(this.value),
|
|
||||||
})}"
|
|
||||||
@drop=${this._handleDrop}
|
|
||||||
@dragenter=${this._handleDragStart}
|
|
||||||
@dragover=${this._handleDragStart}
|
|
||||||
@dragleave=${this._handleDragEnd}
|
|
||||||
@dragend=${this._handleDragEnd}
|
|
||||||
>
|
|
||||||
<span class="mdc-text-field__ripple"></span>
|
|
||||||
<span
|
|
||||||
class="mdc-floating-label ${this.value || this._drag
|
|
||||||
? "mdc-floating-label--float-above"
|
|
||||||
: ""}"
|
|
||||||
id="label"
|
|
||||||
>${this.label}</span
|
|
||||||
>
|
>
|
||||||
${this.icon
|
${this.progress
|
||||||
? html`<span
|
? html`<span class="progress"
|
||||||
class="mdc-text-field__icon mdc-text-field__icon--leading"
|
>${this.progress}${blankBeforePercent(
|
||||||
>
|
this.hass!.locale
|
||||||
<ha-icon-button
|
)}%</span
|
||||||
@click=${this._openFilePicker}
|
>`
|
||||||
.path=${this.icon}
|
|
||||||
></ha-icon-button>
|
|
||||||
</span>`
|
|
||||||
: ""}
|
: ""}
|
||||||
<div class="value">${this.value}</div>
|
</div>
|
||||||
<input
|
<mwc-linear-progress
|
||||||
id="input"
|
.indeterminate=${!this.progress}
|
||||||
type="file"
|
.progress=${this.progress ? this.progress / 100 : undefined}
|
||||||
class="mdc-text-field__input file"
|
></mwc-linear-progress>
|
||||||
accept=${this.accept}
|
</div>`
|
||||||
@change=${this._handleFilePicked}
|
: html`<label
|
||||||
aria-labelledby="label"
|
for=${this.value ? "" : "input"}
|
||||||
/>
|
class="container ${classMap({
|
||||||
${this.value
|
dragged: this._drag,
|
||||||
? html`<span
|
multiple: this.multiple,
|
||||||
class="mdc-text-field__icon mdc-text-field__icon--trailing"
|
value: Boolean(this.value),
|
||||||
|
})}"
|
||||||
|
@drop=${this._handleDrop}
|
||||||
|
@dragenter=${this._handleDragStart}
|
||||||
|
@dragover=${this._handleDragStart}
|
||||||
|
@dragleave=${this._handleDragEnd}
|
||||||
|
@dragend=${this._handleDragEnd}
|
||||||
|
>${!this.value
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
class="big-icon"
|
||||||
|
.path=${this.icon || mdiFileUpload}
|
||||||
|
></ha-svg-icon>
|
||||||
|
<ha-button unelevated @click=${this._openFilePicker}>
|
||||||
|
${this.label ||
|
||||||
|
this.hass?.localize("ui.components.file-upload.label")}
|
||||||
|
</ha-button>
|
||||||
|
<span class="secondary"
|
||||||
|
>${this.secondary ||
|
||||||
|
this.hass?.localize(
|
||||||
|
"ui.components.file-upload.secondary"
|
||||||
|
)}</span
|
||||||
>
|
>
|
||||||
<ha-icon-button
|
<span class="supports">${this.supports}</span>`
|
||||||
slot="suffix"
|
: typeof this.value === "string"
|
||||||
@click=${this._clearValue}
|
? html`<div class="row">
|
||||||
.label=${this.hass?.localize("ui.common.close") ||
|
<div class="value" @click=${this._openFilePicker}>
|
||||||
"close"}
|
<ha-svg-icon
|
||||||
.path=${mdiClose}
|
.path=${this.icon || mdiFileUpload}
|
||||||
></ha-icon-button>
|
></ha-svg-icon>
|
||||||
</span>`
|
${this.value}
|
||||||
: ""}
|
</div>
|
||||||
<span
|
<ha-icon-button
|
||||||
class="mdc-line-ripple ${this._drag
|
@click=${this._clearValue}
|
||||||
? "mdc-line-ripple--active"
|
.label=${this.hass?.localize("ui.common.delete") ||
|
||||||
: ""}"
|
"Delete"}
|
||||||
></span>
|
.path=${mdiDelete}
|
||||||
</label>
|
></ha-icon-button>
|
||||||
`}
|
</div>`
|
||||||
|
: (this.value instanceof FileList
|
||||||
|
? Array.from(this.value)
|
||||||
|
: ensureArray(this.value)
|
||||||
|
).map(
|
||||||
|
(file) =>
|
||||||
|
html`<div class="row">
|
||||||
|
<div class="value" @click=${this._openFilePicker}>
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${this.icon || mdiFileUpload}
|
||||||
|
></ha-svg-icon>
|
||||||
|
${file.name} - ${bytesToString(file.size)}
|
||||||
|
</div>
|
||||||
|
<ha-icon-button
|
||||||
|
@click=${this._clearValue}
|
||||||
|
.label=${this.hass?.localize("ui.common.delete") ||
|
||||||
|
"Delete"}
|
||||||
|
.path=${mdiDelete}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>`
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
id="input"
|
||||||
|
type="file"
|
||||||
|
class="file"
|
||||||
|
.accept=${this.accept}
|
||||||
|
.multiple=${this.multiple}
|
||||||
|
@change=${this._handleFilePicked}
|
||||||
|
/></label>`}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +165,12 @@ export class HaFileUpload extends LitElement {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (ev.dataTransfer?.files) {
|
if (ev.dataTransfer?.files) {
|
||||||
fireEvent(this, "file-picked", { files: ev.dataTransfer.files });
|
fireEvent(this, "file-picked", {
|
||||||
|
files:
|
||||||
|
this.multiple || ev.dataTransfer.files.length === 1
|
||||||
|
? Array.from(ev.dataTransfer.files)
|
||||||
|
: [ev.dataTransfer.files[0]],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this._drag = false;
|
this._drag = false;
|
||||||
}
|
}
|
||||||
@ -140,92 +188,121 @@ export class HaFileUpload extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _handleFilePicked(ev) {
|
private _handleFilePicked(ev) {
|
||||||
|
if (ev.target.files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.value = ev.target.files;
|
||||||
fireEvent(this, "file-picked", { files: ev.target.files });
|
fireEvent(this, "file-picked", { files: ev.target.files });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearValue(ev: Event) {
|
private _clearValue(ev: Event) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.value = null;
|
this._input!.value = "";
|
||||||
|
this.value = undefined;
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return css`
|
||||||
styles,
|
:host {
|
||||||
css`
|
display: block;
|
||||||
:host {
|
height: 240px;
|
||||||
display: block;
|
}
|
||||||
}
|
:host([disabled]) {
|
||||||
.mdc-text-field--filled {
|
pointer-events: none;
|
||||||
height: auto;
|
color: var(--disabled-text-color);
|
||||||
padding-top: 16px;
|
}
|
||||||
cursor: pointer;
|
.container {
|
||||||
}
|
position: relative;
|
||||||
.mdc-text-field--filled.mdc-text-field--with-trailing-icon {
|
display: flex;
|
||||||
padding-top: 28px;
|
flex-direction: column;
|
||||||
}
|
justify-content: center;
|
||||||
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
|
align-items: center;
|
||||||
color: var(--secondary-text-color);
|
border: solid 1px
|
||||||
}
|
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
||||||
.mdc-text-field--filled.mdc-text-field--with-trailing-icon
|
border-radius: var(--mdc-shape-small, 4px);
|
||||||
.mdc-text-field__icon {
|
height: 100%;
|
||||||
align-self: flex-end;
|
}
|
||||||
}
|
label.container {
|
||||||
.mdc-text-field__icon--leading {
|
border: dashed 1px
|
||||||
margin-bottom: 12px;
|
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
||||||
inset-inline-start: initial;
|
cursor: pointer;
|
||||||
inset-inline-end: 0px;
|
}
|
||||||
direction: var(--direction);
|
:host([disabled]) .container {
|
||||||
}
|
border-color: var(--disabled-color);
|
||||||
.mdc-text-field--filled .mdc-floating-label--float-above {
|
}
|
||||||
transform: scale(0.75);
|
label.dragged {
|
||||||
top: 8px;
|
border-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
.mdc-floating-label {
|
.dragged:before {
|
||||||
inset-inline-start: 16px !important;
|
position: absolute;
|
||||||
inset-inline-end: initial !important;
|
top: 0;
|
||||||
direction: var(--direction);
|
right: 0;
|
||||||
}
|
bottom: 0;
|
||||||
.mdc-text-field--filled .mdc-floating-label {
|
left: 0;
|
||||||
inset-inline-start: 48px !important;
|
background-color: var(--primary-color);
|
||||||
inset-inline-end: initial !important;
|
content: "";
|
||||||
direction: var(--direction);
|
opacity: var(--dark-divider-opacity);
|
||||||
}
|
pointer-events: none;
|
||||||
.mdc-text-field__icon--trailing {
|
border-radius: var(--mdc-shape-small, 4px);
|
||||||
pointer-events: auto !important;
|
}
|
||||||
}
|
label.value {
|
||||||
.dragged:before {
|
cursor: default;
|
||||||
position: var(--layout-fit_-_position);
|
}
|
||||||
top: var(--layout-fit_-_top);
|
label.value.multiple {
|
||||||
right: var(--layout-fit_-_right);
|
justify-content: unset;
|
||||||
bottom: var(--layout-fit_-_bottom);
|
overflow: auto;
|
||||||
left: var(--layout-fit_-_left);
|
}
|
||||||
background: currentColor;
|
.highlight {
|
||||||
content: "";
|
color: var(--primary-color);
|
||||||
opacity: var(--dark-divider-opacity);
|
}
|
||||||
pointer-events: none;
|
.row {
|
||||||
border-radius: 4px;
|
display: flex;
|
||||||
}
|
width: 100%;
|
||||||
.value {
|
align-items: center;
|
||||||
width: 100%;
|
justify-content: space-between;
|
||||||
}
|
padding: 0 16px;
|
||||||
input.file {
|
box-sizing: border-box;
|
||||||
display: none;
|
}
|
||||||
}
|
ha-button {
|
||||||
img {
|
margin-bottom: 4px;
|
||||||
max-width: 100%;
|
}
|
||||||
max-height: 125px;
|
.supports {
|
||||||
}
|
color: var(--secondary-text-color);
|
||||||
ha-icon-button {
|
font-size: 12px;
|
||||||
--mdc-icon-button-size: 24px;
|
}
|
||||||
--mdc-icon-size: 20px;
|
:host([disabled]) .secondary {
|
||||||
}
|
color: var(--disabled-text-color);
|
||||||
ha-circular-progress {
|
}
|
||||||
display: block;
|
input.file {
|
||||||
text-align-last: center;
|
display: none;
|
||||||
}
|
}
|
||||||
`,
|
.value {
|
||||||
];
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.value ha-svg-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.big-icon {
|
||||||
|
--mdc-icon-size: 48px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
ha-button {
|
||||||
|
--mdc-button-outline-color: var(--primary-color);
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
}
|
||||||
|
mwc-linear-progress {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,11 +47,12 @@ export const computeInitialHaFormData = (
|
|||||||
} else if ("boolean" in selector) {
|
} else if ("boolean" in selector) {
|
||||||
data[field.name] = false;
|
data[field.name] = false;
|
||||||
} else if (
|
} else if (
|
||||||
"text" in selector ||
|
|
||||||
"addon" in selector ||
|
"addon" in selector ||
|
||||||
"attribute" in selector ||
|
"attribute" in selector ||
|
||||||
"file" in selector ||
|
"file" in selector ||
|
||||||
"icon" in selector ||
|
"icon" in selector ||
|
||||||
|
"template" in selector ||
|
||||||
|
"text" in selector ||
|
||||||
"theme" in selector
|
"theme" in selector
|
||||||
) {
|
) {
|
||||||
data[field.name] = "";
|
data[field.name] = "";
|
||||||
@ -59,7 +60,8 @@ export const computeInitialHaFormData = (
|
|||||||
data[field.name] = selector.number?.min ?? 0;
|
data[field.name] = selector.number?.min ?? 0;
|
||||||
} else if ("select" in selector) {
|
} else if ("select" in selector) {
|
||||||
if (selector.select?.options.length) {
|
if (selector.select?.options.length) {
|
||||||
data[field.name] = selector.select.options[0][0];
|
const val = selector.select.options[0];
|
||||||
|
data[field.name] = Array.isArray(val) ? val[0] : val;
|
||||||
}
|
}
|
||||||
} else if ("duration" in selector) {
|
} else if ("duration" in selector) {
|
||||||
data[field.name] = {
|
data[field.name] = {
|
||||||
|
@ -68,6 +68,7 @@ export class HaFormString extends LitElement implements HaFormElement {
|
|||||||
: this.schema.description?.suffix}
|
: this.schema.description?.suffix}
|
||||||
.validationMessage=${this.schema.required ? "Required" : undefined}
|
.validationMessage=${this.schema.required ? "Required" : undefined}
|
||||||
@input=${this._valueChanged}
|
@input=${this._valueChanged}
|
||||||
|
@change=${this._valueChanged}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
${isPassword
|
${isPassword
|
||||||
? html`<ha-icon-button
|
? html`<ha-icon-button
|
||||||
|
@ -2,8 +2,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
|
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
|
||||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||||
import { formatNumber } from "../common/number/format_number";
|
|
||||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
|
||||||
import { isUnavailableState, OFF } from "../data/entity";
|
import { isUnavailableState, OFF } from "../data/entity";
|
||||||
import { HumidifierEntity } from "../data/humidifier";
|
import { HumidifierEntity } from "../data/humidifier";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
@ -51,10 +49,10 @@ class HaHumidifierState extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.stateObj.attributes.current_humidity != null) {
|
if (this.stateObj.attributes.current_humidity != null) {
|
||||||
return `${formatNumber(
|
return `${this.hass.formatEntityAttributeValue(
|
||||||
this.stateObj.attributes.current_humidity,
|
this.stateObj,
|
||||||
this.hass.locale
|
"current_humidity"
|
||||||
)}${blankBeforePercent(this.hass.locale)}%`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -66,10 +64,10 @@ class HaHumidifierState extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.stateObj.attributes.humidity != null) {
|
if (this.stateObj.attributes.humidity != null) {
|
||||||
return `${formatNumber(
|
return `${this.hass.formatEntityAttributeValue(
|
||||||
this.stateObj.attributes.humidity,
|
this.stateObj,
|
||||||
this.hass.locale
|
"humidity"
|
||||||
)}${blankBeforePercent(this.hass.locale)}%`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
|
@ -14,17 +14,17 @@ export class HaIconButtonGroup extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 56px;
|
height: 48px;
|
||||||
border-radius: 28px;
|
border-radius: 28px;
|
||||||
background-color: rgba(139, 145, 151, 0.1);
|
background-color: rgba(139, 145, 151, 0.1);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: auto;
|
width: auto;
|
||||||
padding: 4px;
|
padding: 0;
|
||||||
gap: 4px;
|
|
||||||
}
|
}
|
||||||
::slotted(.separator) {
|
::slotted(.separator) {
|
||||||
background-color: rgba(var(--rgb-primary-text-color), 0.15);
|
background-color: rgba(var(--rgb-primary-text-color), 0.15);
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
margin: 0 1px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -38,7 +38,7 @@ export class HaIconButtonToggle extends HaIconButton {
|
|||||||
:host([selected]) mwc-icon-button {
|
:host([selected]) mwc-icon-button {
|
||||||
color: var(--primary-background-color);
|
color: var(--primary-background-color);
|
||||||
}
|
}
|
||||||
:host([selected]) mwc-icon-button::before {
|
:host([selected]:not([disabled])) mwc-icon-button::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -7,6 +7,7 @@ import { formatLanguageCode } from "../common/language/format_language";
|
|||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
import { FrontendLocaleData } from "../data/translation";
|
import { FrontendLocaleData } from "../data/translation";
|
||||||
import "../resources/intl-polyfill";
|
import "../resources/intl-polyfill";
|
||||||
|
import { translationMetadata } from "../resources/translations-metadata";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./ha-list-item";
|
import "./ha-list-item";
|
||||||
import "./ha-select";
|
import "./ha-select";
|
||||||
@ -20,7 +21,7 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
|
|
||||||
@property() public languages?: string[];
|
@property() public languages?: string[];
|
||||||
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||||
|
|
||||||
@ -41,7 +42,18 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
|
|
||||||
protected updated(changedProperties: PropertyValues) {
|
protected updated(changedProperties: PropertyValues) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
if (changedProperties.has("languages") || changedProperties.has("value")) {
|
|
||||||
|
const localeChanged =
|
||||||
|
changedProperties.has("hass") &&
|
||||||
|
this.hass &&
|
||||||
|
changedProperties.get("hass") &&
|
||||||
|
changedProperties.get("hass").locale.language !==
|
||||||
|
this.hass.locale.language;
|
||||||
|
if (
|
||||||
|
changedProperties.has("languages") ||
|
||||||
|
changedProperties.has("value") ||
|
||||||
|
localeChanged
|
||||||
|
) {
|
||||||
this._select.layoutOptions();
|
this._select.layoutOptions();
|
||||||
if (this._select.value !== this.value) {
|
if (this._select.value !== this.value) {
|
||||||
fireEvent(this, "value-changed", { value: this._select.value });
|
fireEvent(this, "value-changed", { value: this._select.value });
|
||||||
@ -51,24 +63,27 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
}
|
}
|
||||||
const languageOptions = this._getLanguagesOptions(
|
const languageOptions = this._getLanguagesOptions(
|
||||||
this.languages ?? this._defaultLanguages,
|
this.languages ?? this._defaultLanguages,
|
||||||
this.hass.locale,
|
this.nativeName,
|
||||||
this.nativeName
|
this.hass?.locale
|
||||||
);
|
);
|
||||||
const selectedItem = languageOptions.find(
|
const selectedItemIndex = languageOptions.findIndex(
|
||||||
(option) => option.value === this.value
|
(option) => option.value === this.value
|
||||||
);
|
);
|
||||||
if (!selectedItem) {
|
if (selectedItemIndex === -1) {
|
||||||
this.value = undefined;
|
this.value = undefined;
|
||||||
}
|
}
|
||||||
|
if (localeChanged) {
|
||||||
|
this._select.select(selectedItemIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getLanguagesOptions = memoizeOne(
|
private _getLanguagesOptions = memoizeOne(
|
||||||
(languages: string[], locale: FrontendLocaleData, nativeName: boolean) => {
|
(languages: string[], nativeName: boolean, locale?: FrontendLocaleData) => {
|
||||||
let options: { label: string; value: string }[] = [];
|
let options: { label: string; value: string }[] = [];
|
||||||
|
|
||||||
if (nativeName) {
|
if (nativeName) {
|
||||||
const translations = this.hass.translationMetadata.translations;
|
const translations = translationMetadata.translations;
|
||||||
options = languages.map((lang) => {
|
options = languages.map((lang) => {
|
||||||
let label = translations[lang]?.nativeName;
|
let label = translations[lang]?.nativeName;
|
||||||
if (!label) {
|
if (!label) {
|
||||||
@ -87,14 +102,14 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
label,
|
label,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else {
|
} else if (locale) {
|
||||||
options = languages.map((lang) => ({
|
options = languages.map((lang) => ({
|
||||||
value: lang,
|
value: lang,
|
||||||
label: formatLanguageCode(lang, locale),
|
label: formatLanguageCode(lang, locale),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.noSort) {
|
if (!this.noSort && locale) {
|
||||||
options.sort((a, b) =>
|
options.sort((a, b) =>
|
||||||
caseInsensitiveStringCompare(a.label, b.label, locale.language)
|
caseInsensitiveStringCompare(a.label, b.label, locale.language)
|
||||||
);
|
);
|
||||||
@ -104,20 +119,14 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private _computeDefaultLanguageOptions() {
|
private _computeDefaultLanguageOptions() {
|
||||||
if (!this.hass.translationMetadata?.translations) {
|
this._defaultLanguages = Object.keys(translationMetadata.translations);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._defaultLanguages = Object.keys(
|
|
||||||
this.hass.translationMetadata.translations
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const languageOptions = this._getLanguagesOptions(
|
const languageOptions = this._getLanguagesOptions(
|
||||||
this.languages ?? this._defaultLanguages,
|
this.languages ?? this._defaultLanguages,
|
||||||
this.hass.locale,
|
this.nativeName,
|
||||||
this.nativeName
|
this.hass?.locale
|
||||||
);
|
);
|
||||||
|
|
||||||
const value =
|
const value =
|
||||||
@ -125,9 +134,10 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-select
|
<ha-select
|
||||||
.label=${this.label ||
|
.label=${this.label ??
|
||||||
this.hass.localize("ui.components.language-picker.language")}
|
(this.hass?.localize("ui.components.language-picker.language") ||
|
||||||
.value=${value}
|
"Language")}
|
||||||
|
.value=${value || ""}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@selected=${this._changed}
|
@selected=${this._changed}
|
||||||
@ -137,9 +147,9 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
>
|
>
|
||||||
${languageOptions.length === 0
|
${languageOptions.length === 0
|
||||||
? html`<ha-list-item value=""
|
? html`<ha-list-item value=""
|
||||||
>${this.hass.localize(
|
>${this.hass?.localize(
|
||||||
"ui.components.language-picker.no_languages"
|
"ui.components.language-picker.no_languages"
|
||||||
)}</ha-list-item
|
) || "No languages"}</ha-list-item
|
||||||
>`
|
>`
|
||||||
: languageOptions.map(
|
: languageOptions.map(
|
||||||
(option) => html`
|
(option) => html`
|
||||||
@ -162,7 +172,7 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
|
|
||||||
private _changed(ev): void {
|
private _changed(ev): void {
|
||||||
const target = ev.target as HaSelect;
|
const target = ev.target as HaSelect;
|
||||||
if (!this.hass || target.value === "" || target.value === this.value) {
|
if (target.value === "" || target.value === this.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.value = target.value;
|
this.value = target.value;
|
||||||
|
91
src/components/ha-lawn_mower-action-button.ts
Normal file
91
src/components/ha-lawn_mower-action-button.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import { CSSResultGroup, LitElement, css, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { supportsFeature } from "../common/entity/supports-feature";
|
||||||
|
import {
|
||||||
|
LawnMowerEntity,
|
||||||
|
LawnMowerEntityFeature,
|
||||||
|
LawnMowerEntityState,
|
||||||
|
} from "../data/lawn_mower";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
type LawnMowerAction = {
|
||||||
|
action: string;
|
||||||
|
service: string;
|
||||||
|
feature: LawnMowerEntityFeature;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LAWN_MOWER_ACTIONS: Partial<
|
||||||
|
Record<LawnMowerEntityState, LawnMowerAction>
|
||||||
|
> = {
|
||||||
|
mowing: {
|
||||||
|
action: "dock",
|
||||||
|
service: "dock",
|
||||||
|
feature: LawnMowerEntityFeature.DOCK,
|
||||||
|
},
|
||||||
|
docked: {
|
||||||
|
action: "start_mowing",
|
||||||
|
service: "start_mowing",
|
||||||
|
feature: LawnMowerEntityFeature.START_MOWING,
|
||||||
|
},
|
||||||
|
paused: {
|
||||||
|
action: "resume_mowing",
|
||||||
|
service: "start_mowing",
|
||||||
|
feature: LawnMowerEntityFeature.START_MOWING,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("ha-lawn_mower-action-button")
|
||||||
|
class HaLawnMowerActionButton extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public stateObj!: LawnMowerEntity;
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const state = this.stateObj.state;
|
||||||
|
const action = LAWN_MOWER_ACTIONS[state];
|
||||||
|
|
||||||
|
if (action && supportsFeature(this.stateObj, action.feature)) {
|
||||||
|
return html`
|
||||||
|
<mwc-button @click=${this.callService} .service=${action.service}>
|
||||||
|
${this.hass.localize(`ui.card.lawn_mower.actions.${action.action}`)}
|
||||||
|
</mwc-button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<mwc-button disabled>
|
||||||
|
${this.hass.formatEntityState(this.stateObj)}
|
||||||
|
</mwc-button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
callService(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const stateObj = this.stateObj;
|
||||||
|
const service = ev.target.service;
|
||||||
|
this.hass.callService("lawn_mower", service, {
|
||||||
|
entity_id: stateObj.entity_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
mwc-button {
|
||||||
|
top: 3px;
|
||||||
|
height: 37px;
|
||||||
|
margin-right: -0.57em;
|
||||||
|
}
|
||||||
|
mwc-button[disabled] {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-lawn_mower-action-button": HaLawnMowerActionButton;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import { css } from "lit";
|
import { css } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import { OutlinedButton } from "@material/web/button/lib/outlined-button";
|
import { OutlinedButton } from "@material/web/button/internal/outlined-button";
|
||||||
import { styles as outlinedStyles } from "@material/web/button/lib/outlined-styles.css";
|
import { styles as outlinedStyles } from "@material/web/button/internal/outlined-styles.css";
|
||||||
import { styles as sharedStyles } from "@material/web/button/lib/shared-styles.css";
|
import { styles as sharedStyles } from "@material/web/button/internal/shared-styles.css";
|
||||||
|
|
||||||
@customElement("ha-outlined-button")
|
@customElement("ha-outlined-button")
|
||||||
export class HaOutlinedButton extends OutlinedButton {
|
export class HaOutlinedButton extends OutlinedButton {
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { css } from "lit";
|
import { css } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
|
import { IconButton } from "@material/web/iconbutton/internal/icon-button";
|
||||||
import { IconButton } from "@material/web/iconbutton/lib/icon-button";
|
import { styles as outlinedStyles } from "@material/web/iconbutton/internal/outlined-styles.css";
|
||||||
import { styles as outlinedStyles } from "@material/web/iconbutton/lib/outlined-styles.css";
|
import { styles as sharedStyles } from "@material/web/iconbutton/internal/shared-styles.css";
|
||||||
import { styles as sharedStyles } from "@material/web/iconbutton/lib/shared-styles.css";
|
|
||||||
|
|
||||||
@customElement("ha-outlined-icon-button")
|
@customElement("ha-outlined-icon-button")
|
||||||
export class HaOutlinedIconButton extends IconButton {
|
export class HaOutlinedIconButton extends IconButton {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { mdiImagePlus } from "@mdi/js";
|
import { mdiImagePlus } from "@mdi/js";
|
||||||
import { html, LitElement, TemplateResult } from "lit";
|
import { LitElement, TemplateResult, css, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
|
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
|
||||||
@ -9,6 +9,7 @@ import {
|
|||||||
showImageCropperDialog,
|
showImageCropperDialog,
|
||||||
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
import "./ha-button";
|
||||||
import "./ha-circular-progress";
|
import "./ha-circular-progress";
|
||||||
import "./ha-file-upload";
|
import "./ha-file-upload";
|
||||||
|
|
||||||
@ -20,6 +21,12 @@ export class HaPictureUpload extends LitElement {
|
|||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public secondary?: string;
|
||||||
|
|
||||||
|
@property() public supports?: string;
|
||||||
|
|
||||||
|
@property() public currentImageAltText?: string;
|
||||||
|
|
||||||
@property({ type: Boolean }) public crop = false;
|
@property({ type: Boolean }) public crop = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public cropOptions?: CropOptions;
|
@property({ attribute: false }) public cropOptions?: CropOptions;
|
||||||
@ -29,19 +36,44 @@ export class HaPictureUpload extends LitElement {
|
|||||||
@state() private _uploading = false;
|
@state() private _uploading = false;
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
if (!this.value) {
|
||||||
<ha-file-upload
|
return html`
|
||||||
.hass=${this.hass}
|
<ha-file-upload
|
||||||
.icon=${mdiImagePlus}
|
.hass=${this.hass}
|
||||||
.label=${this.label ||
|
.icon=${mdiImagePlus}
|
||||||
this.hass.localize("ui.components.picture-upload.label")}
|
.label=${this.label ||
|
||||||
.uploading=${this._uploading}
|
this.hass.localize("ui.components.picture-upload.label")}
|
||||||
.value=${this.value ? html`<img .src=${this.value} />` : ""}
|
.secondary=${this.secondary}
|
||||||
@file-picked=${this._handleFilePicked}
|
.supports=${this.supports ||
|
||||||
@change=${this._handleFileCleared}
|
this.hass.localize("ui.components.picture-upload.supported_formats")}
|
||||||
accept="image/png, image/jpeg, image/gif"
|
.uploading=${this._uploading}
|
||||||
></ha-file-upload>
|
@file-picked=${this._handleFilePicked}
|
||||||
`;
|
@change=${this._handleFileCleared}
|
||||||
|
accept="image/png, image/jpeg, image/gif"
|
||||||
|
></ha-file-upload>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return html`<div class="center-vertical">
|
||||||
|
<div class="value">
|
||||||
|
<img
|
||||||
|
.src=${this.value}
|
||||||
|
alt=${this.currentImageAltText ||
|
||||||
|
this.hass.localize("ui.components.picture-upload.current_image_alt")}
|
||||||
|
/>
|
||||||
|
<ha-button
|
||||||
|
@click=${this._handleChangeClick}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.components.picture-upload.change_picture"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleChangeClick() {
|
||||||
|
this.value = null;
|
||||||
|
fireEvent(this, "change");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _handleFilePicked(ev) {
|
private async _handleFilePicked(ev) {
|
||||||
@ -100,6 +132,35 @@ export class HaPictureUpload extends LitElement {
|
|||||||
this._uploading = false;
|
this._uploading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
height: 240px;
|
||||||
|
}
|
||||||
|
ha-file-upload {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.center-vertical {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: var(--file-upload-image-border-radius);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -47,6 +47,9 @@ export class HaSelect extends SelectBase {
|
|||||||
.mdc-select__anchor {
|
.mdc-select__anchor {
|
||||||
width: var(--ha-select-min-width, 200px);
|
width: var(--ha-select-min-width, 200px);
|
||||||
}
|
}
|
||||||
|
.mdc-select--filled .mdc-select__anchor {
|
||||||
|
height: var(--ha-select-height, 56px);
|
||||||
|
}
|
||||||
.mdc-select--filled .mdc-floating-label {
|
.mdc-select--filled .mdc-floating-label {
|
||||||
inset-inline-start: 12px;
|
inset-inline-start: 12px;
|
||||||
inset-inline-end: initial;
|
inset-inline-end: initial;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { Action } from "../../data/script";
|
import { Action } from "../../data/script";
|
||||||
import { ActionSelector } from "../../data/selector";
|
import { ActionSelector } from "../../data/selector";
|
||||||
@ -19,10 +19,13 @@ export class HaActionSelector extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||||
<ha-automation-action
|
<ha-automation-action
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.actions=${this.value || []}
|
.actions=${this.value || []}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.nested=${this.selector.action?.nested}
|
||||||
|
.reOrderMode=${this.selector.action?.reorder_mode}
|
||||||
></ha-automation-action>
|
></ha-automation-action>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -37,6 +40,11 @@ export class HaActionSelector extends LitElement {
|
|||||||
opacity: var(--light-disabled-opacity);
|
opacity: var(--light-disabled-opacity);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { Condition } from "../../data/automation";
|
import { Condition } from "../../data/automation";
|
||||||
import { ConditionSelector } from "../../data/selector";
|
import { ConditionSelector } from "../../data/selector";
|
||||||
@ -19,10 +19,13 @@ export class HaConditionSelector extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||||
<ha-automation-condition
|
<ha-automation-condition
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.conditions=${this.value || []}
|
.conditions=${this.value || []}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.nested=${this.selector.condition?.nested}
|
||||||
|
.reOrderMode=${this.selector.condition?.reorder_mode}
|
||||||
></ha-automation-condition>
|
></ha-automation-condition>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -37,6 +40,11 @@ export class HaConditionSelector extends LitElement {
|
|||||||
opacity: var(--light-disabled-opacity);
|
opacity: var(--light-disabled-opacity);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,9 +37,12 @@ export class HaFileSelector extends LitElement {
|
|||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.helper=${this.helper}
|
.supports=${this.helper}
|
||||||
.uploading=${this._busy}
|
.uploading=${this._busy}
|
||||||
.value=${this.value ? this._filename?.name || "Unknown file" : ""}
|
.value=${this.value
|
||||||
|
? this._filename?.name ||
|
||||||
|
this.hass.localize("ui.components.selectors.file.unknown_file")
|
||||||
|
: undefined}
|
||||||
@file-picked=${this._uploadFile}
|
@file-picked=${this._uploadFile}
|
||||||
@change=${this._removeFile}
|
@change=${this._removeFile}
|
||||||
></ha-file-upload>
|
></ha-file-upload>
|
||||||
|
@ -16,6 +16,7 @@ import "../ha-formfield";
|
|||||||
import "../ha-radio";
|
import "../ha-radio";
|
||||||
import "../ha-select";
|
import "../ha-select";
|
||||||
import "../ha-input-helper-text";
|
import "../ha-input-helper-text";
|
||||||
|
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||||
|
|
||||||
@customElement("ha-selector-select")
|
@customElement("ha-selector-select")
|
||||||
export class HaSelectSelector extends LitElement {
|
export class HaSelectSelector extends LitElement {
|
||||||
@ -51,12 +52,25 @@ export class HaSelectSelector extends LitElement {
|
|||||||
|
|
||||||
if (this.localizeValue && translationKey) {
|
if (this.localizeValue && translationKey) {
|
||||||
options.forEach((option) => {
|
options.forEach((option) => {
|
||||||
option.label =
|
const localizedLabel = this.localizeValue!(
|
||||||
this.localizeValue!(`${translationKey}.options.${option.value}`) ||
|
`${translationKey}.options.${option.value}`
|
||||||
option.label;
|
);
|
||||||
|
if (localizedLabel) {
|
||||||
|
option.label = localizedLabel;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.selector.select?.sort) {
|
||||||
|
options.sort((a, b) =>
|
||||||
|
caseInsensitiveStringCompare(
|
||||||
|
a.label,
|
||||||
|
b.label,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.selector.select?.custom_value && this._mode === "list") {
|
if (!this.selector.select?.custom_value && this._mode === "list") {
|
||||||
if (!this.selector.select?.multiple) {
|
if (!this.selector.select?.multiple) {
|
||||||
return html`
|
return html`
|
||||||
|
@ -1075,7 +1075,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0px 6px;
|
padding: 0px 2px;
|
||||||
color: var(--text-accent-color, var(--text-primary-color));
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
}
|
}
|
||||||
ha-svg-icon + .notification-badge,
|
ha-svg-icon + .notification-badge,
|
||||||
|
@ -65,6 +65,7 @@ export class HaTabs extends PaperTabs {
|
|||||||
const selected = this.querySelector(".iron-selected");
|
const selected = this.querySelector(".iron-selected");
|
||||||
if (selected) {
|
if (selected) {
|
||||||
selected.scrollIntoView();
|
selected.scrollIntoView();
|
||||||
|
this._affectScroll(0); // Ensure scroll arrows match scroll position
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
|
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
|
||||||
import { css, html, LitElement, PropertyValues, svg } from "lit";
|
import { LitElement, PropertyValues, css, html, svg } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { rgb2hex } from "../common/color/convert-color";
|
import { rgb2hex } from "../common/color/convert-color";
|
||||||
import { temperature2rgb } from "../common/color/convert-light-color";
|
import {
|
||||||
|
DEFAULT_MAX_KELVIN,
|
||||||
|
DEFAULT_MIN_KELVIN,
|
||||||
|
temperature2rgb,
|
||||||
|
} from "../common/color/convert-light-color";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
|
||||||
const SAFE_ZONE_FACTOR = 0.9;
|
const SAFE_ZONE_FACTOR = 0.9;
|
||||||
@ -79,10 +83,10 @@ class HaTempColorPicker extends LitElement {
|
|||||||
public value?: number;
|
public value?: number;
|
||||||
|
|
||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
public min = 2000;
|
public min = DEFAULT_MIN_KELVIN;
|
||||||
|
|
||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
public max = 10000;
|
public max = DEFAULT_MAX_KELVIN;
|
||||||
|
|
||||||
@query("#canvas") private _canvas!: HTMLCanvasElement;
|
@query("#canvas") private _canvas!: HTMLCanvasElement;
|
||||||
|
|
||||||
|
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