20231025.0 (#18395)

This commit is contained in:
Bram Kragten 2023-10-25 12:17:22 +02:00 committed by GitHub
commit fdaefadd18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
276 changed files with 9587 additions and 5309 deletions

View File

@ -21,7 +21,7 @@ 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@v4.1.0 uses: actions/checkout@v4.1.1
with: with:
ref: dev ref: dev
@ -57,7 +57,7 @@ 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@v4.1.0 uses: actions/checkout@v4.1.1
with: with:
ref: master ref: master

View File

@ -24,7 +24,7 @@ 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@v4.1.0 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1
with: with:
@ -55,7 +55,7 @@ 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@v4.1.0 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1
with: with:
@ -73,7 +73,7 @@ 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@v4.1.0 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1
with: with:
@ -85,13 +85,19 @@ jobs:
run: ./node_modules/.bin/gulp build-app run: ./node_modules/.bin/gulp build-app
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v3.1.3
with:
name: frontend-bundle-stats
path: build/stats/*.json
if-no-files-found: error
supervisor: supervisor:
name: Build supervisor name: Build supervisor
needs: [lint, test] needs: [lint, test]
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@v4.1.0 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1
with: with:
@ -103,3 +109,9 @@ jobs:
run: ./node_modules/.bin/gulp build-hassio run: ./node_modules/.bin/gulp build-hassio
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v3.1.3
with:
name: supervisor-bundle-stats
path: build/stats/*.json
if-no-files-found: error

View File

@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.1.0 uses: actions/checkout@v4.1.1
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.

View File

@ -22,7 +22,7 @@ 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@v4.1.0 uses: actions/checkout@v4.1.1
with: with:
ref: dev ref: dev
@ -58,7 +58,7 @@ 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@v4.1.0 uses: actions/checkout@v4.1.1
with: with:
ref: master ref: master

View File

@ -16,7 +16,7 @@ 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@v4.1.0 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1

View File

@ -21,7 +21,7 @@ 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@v4.1.0 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1

View File

@ -20,7 +20,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.0 uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4 uses: actions/setup-python@v4
@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3.1.3
with: with:
name: wheels name: wheels
path: dist/home_assistant_frontend*.whl path: dist/home_assistant_frontend*.whl
if-no-files-found: error if-no-files-found: error
- name: Upload translations - name: Upload translations
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3.1.3
with: with:
name: translations name: translations
path: translations.tar.gz path: translations.tar.gz

25
.github/workflows/relative-ci.yaml vendored Normal file
View File

@ -0,0 +1,25 @@
name: RelativeCI
on:
workflow_run:
workflows: [CI]
types:
- completed
jobs:
upload:
name: Upload stats
if: ${{ github.event.workflow_run.conclusion == 'success' }}
strategy:
matrix:
bundle: [frontend, supervisor]
build: [modern, legacy]
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@v2.1.10
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}
artifactName: ${{ format('{0}-bundle-stats', matrix.bundle) }}
webpackStatsFile: ${{ format('{0}-{1}.json', matrix.bundle, matrix.build) }}

View File

@ -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@v4.1.0 uses: actions/checkout@v4.1.1
- name: Verify version - name: Verify version
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
@ -74,7 +74,7 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2023.10.1 uses: home-assistant/wheels@2023.10.5
with: with:
abi: cp311 abi: cp311
tag: musllinux_1_2 tag: musllinux_1_2

View File

@ -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@v4.1.0 uses: actions/checkout@v4.1.1
- name: Upload Translations - name: Upload Translations
run: | run: |

3
.gitignore vendored
View File

@ -47,3 +47,6 @@ src/cast/dev_const.ts
# Home Assistant config # Home Assistant config
/config/ /config/
# Jetbrains
/.idea/

File diff suppressed because one or more lines are too long

View File

@ -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.3.cjs yarnPath: .yarn/releases/yarn-3.6.4.cjs

View File

@ -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.32", proposals: true }, corejs: latestBuild ? false : { version: "3.33", proposals: true },
bugfixes: true, bugfixes: true,
shippedProposals: true, shippedProposals: true,
}, },
@ -149,7 +149,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
sourceMaps: !isTestBuild, sourceMaps: !isTestBuild,
}); });
const nameSuffix = (latestBuild) => (latestBuild ? "-latest" : "-es5"); const nameSuffix = (latestBuild) => (latestBuild ? "-modern" : "-legacy");
const outputPath = (outputRoot, latestBuild) => const outputPath = (outputRoot, latestBuild) =>
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5"); path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
@ -183,7 +183,7 @@ const publicPath = (latestBuild, root = "") =>
module.exports.config = { module.exports.config = {
app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) { app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) {
return { return {
name: "app" + nameSuffix(latestBuild), name: "frontend" + nameSuffix(latestBuild),
entry: { entry: {
service_worker: "./src/entrypoints/service_worker.ts", service_worker: "./src/entrypoints/service_worker.ts",
app: "./src/entrypoints/app.ts", app: "./src/entrypoints/app.ts",

View File

@ -4,6 +4,7 @@ import fs from "fs-extra";
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";
import env from "../env.cjs";
const npmPath = (...parts) => const npmPath = (...parts) =>
path.resolve(paths.polymer_dir, "node_modules", ...parts); path.resolve(paths.polymer_dir, "node_modules", ...parts);
@ -62,6 +63,9 @@ function copyPolyfills(staticDir) {
} }
function copyLoaderJS(staticDir) { function copyLoaderJS(staticDir) {
if (!env.useRollup()) {
return;
}
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
copyFileDir(npmPath("systemjs/dist/s.min.js"), staticPath("js")); copyFileDir(npmPath("systemjs/dist/s.min.js"), staticPath("js"));
copyFileDir(npmPath("systemjs/dist/s.min.js.map"), staticPath("js")); copyFileDir(npmPath("systemjs/dist/s.min.js.map"), staticPath("js"));

View File

@ -1,51 +1,54 @@
import { deleteSync } from "del"; import { deleteSync } from "del";
import { mkdir, readFile, writeFile } from "fs/promises"; import { mkdir, readFile, writeFile } from "fs/promises";
import gulp from "gulp"; import gulp from "gulp";
import path from "path"; import { join, resolve } from "node:path";
import paths from "../paths.cjs"; import paths from "../paths.cjs";
const outDir = path.join(paths.build_dir, "locale-data"); const formatjsDir = join(paths.polymer_dir, "node_modules", "@formatjs");
const outDir = join(paths.build_dir, "locale-data");
const INTL_PACKAGES = { const INTL_POLYFILLS = {
"intl-relativetimeformat": "RelativeTimeFormat",
"intl-datetimeformat": "DateTimeFormat", "intl-datetimeformat": "DateTimeFormat",
"intl-numberformat": "NumberFormat",
"intl-displaynames": "DisplayNames", "intl-displaynames": "DisplayNames",
"intl-listformat": "ListFormat", "intl-listformat": "ListFormat",
"intl-numberformat": "NumberFormat",
"intl-relativetimeformat": "RelativeTimeFormat",
}; };
const convertToJSON = async (pkg, lang) => { const convertToJSON = async (
pkg,
lang,
subDir = "locale-data",
addFunc = "__addLocaleData",
skipMissing = true
) => {
let localeData; let localeData;
try { try {
localeData = await readFile( localeData = await readFile(
path.resolve( join(formatjsDir, pkg, subDir, `${lang}.js`),
paths.polymer_dir,
`node_modules/@formatjs/${pkg}/locale-data/${lang}.js`
),
"utf-8" "utf-8"
); );
} catch (e) { } catch (e) {
// Ignore if language is missing (i.e. not supported by @formatjs) // Ignore if language is missing (i.e. not supported by @formatjs)
if (e.code === "ENOENT") { if (e.code === "ENOENT" && skipMissing) {
console.warn(`Skipped missing data for language ${lang} from ${pkg}`);
return; return;
} else {
throw e;
} }
throw e;
} }
// Convert to JSON // Convert to JSON
const className = INTL_PACKAGES[pkg]; const obj = INTL_POLYFILLS[pkg];
localeData = localeData const dataRegex = new RegExp(
.replace( `Intl\\.${obj}\\.${addFunc}\\((?<data>.*)\\)`,
new RegExp( "s"
`\\/\\*\\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" localeData = localeData.match(dataRegex)?.groups?.data;
), if (!localeData) {
"" throw Error(`Failed to extract data for language ${lang} from ${pkg}`);
) }
.replace(/\)\s*}/im, "");
// Parse to validate JSON, then stringify to minify // Parse to validate JSON, then stringify to minify
localeData = JSON.stringify(JSON.parse(localeData)); localeData = JSON.stringify(JSON.parse(localeData));
await writeFile(path.join(outDir, `${pkg}/${lang}.json`), localeData); await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData);
}; };
gulp.task("clean-locale-data", async () => deleteSync([outDir])); gulp.task("clean-locale-data", async () => deleteSync([outDir]));
@ -53,17 +56,27 @@ gulp.task("clean-locale-data", async () => deleteSync([outDir]));
gulp.task("create-locale-data", async () => { gulp.task("create-locale-data", async () => {
const translationMeta = JSON.parse( const translationMeta = JSON.parse(
await readFile( await readFile(
path.resolve(paths.translations_src, "translationMetadata.json"), resolve(paths.translations_src, "translationMetadata.json"),
"utf-8" "utf-8"
) )
); );
const conversions = []; const conversions = [];
for (const pkg of Object.keys(INTL_PACKAGES)) { for (const pkg of Object.keys(INTL_POLYFILLS)) {
await mkdir(path.join(outDir, pkg), { recursive: true }); // eslint-disable-next-line no-await-in-loop
await mkdir(join(outDir, pkg), { recursive: true });
for (const lang of Object.keys(translationMeta)) { for (const lang of Object.keys(translationMeta)) {
conversions.push(convertToJSON(pkg, lang)); conversions.push(convertToJSON(pkg, lang));
} }
} }
conversions.push(
convertToJSON(
"intl-datetimeformat",
"add-all-tz",
".",
"__addTZData",
false
)
);
await Promise.all(conversions); await Promise.all(conversions);
}); });

View File

@ -1,12 +1,7 @@
import { createHash } from "crypto"; import { createHash } from "crypto";
import { deleteSync } from "del"; import { deleteSync } from "del";
import { import { mkdirSync, readdirSync, readFileSync, renameSync } from "fs";
mkdirSync, import { writeFile } from "node:fs/promises";
readdirSync,
readFileSync,
renameSync,
writeFile,
} from "fs";
import gulp from "gulp"; import gulp from "gulp";
import flatmap from "gulp-flatmap"; import flatmap from "gulp-flatmap";
import transform from "gulp-json-transform"; import transform from "gulp-json-transform";
@ -136,27 +131,23 @@ gulp.task("ensure-translations-build-dir", async () => {
mkdirSync(workDir, { recursive: true }); mkdirSync(workDir, { recursive: true });
}); });
gulp.task("create-test-metadata", (cb) => { gulp.task("create-test-metadata", () =>
writeFile( env.isProdBuild()
workDir + "/testMetadata.json", ? Promise.resolve()
JSON.stringify({ : writeFile(
test: { workDir + "/testMetadata.json",
nativeName: "Test", JSON.stringify({ test: { nativeName: "Test" } })
}, )
}), );
cb
);
});
gulp.task( gulp.task("create-test-translation", () =>
"create-test-translation", env.isProdBuild()
gulp.series("create-test-metadata", () => ? Promise.resolve()
gulp : gulp
.src(path.join(paths.translations_src, "en.json")) .src(path.join(paths.translations_src, "en.json"))
.pipe(transform((data, _file) => recursiveEmpty(data))) .pipe(transform((data, _file) => recursiveEmpty(data)))
.pipe(rename("test.json")) .pipe(rename("test.json"))
.pipe(gulp.dest(workDir)) .pipe(gulp.dest(workDir))
)
); );
/** /**
@ -188,16 +179,11 @@ gulp.task("build-master-translation", () => {
gulp.task("build-merged-translations", () => gulp.task("build-merged-translations", () =>
gulp gulp
.src( .src([
[ inFrontendDir + "/*.json",
inFrontendDir + "/*.json", "!" + inFrontendDir + "/en.json",
"!" + inFrontendDir + "/en.json", ...(env.isProdBuild() ? [] : [workDir + "/test.json"]),
workDir + "/test.json", ])
],
{
allowEmpty: true,
}
)
.pipe(transform((data, file) => lokaliseTransform(data, data, file))) .pipe(transform((data, file) => lokaliseTransform(data, data, file)))
.pipe( .pipe(
flatmap((stream, file) => { flatmap((stream, file) => {
@ -377,14 +363,11 @@ gulp.task("build-translation-flatten-supervisor", () =>
gulp.task("build-translation-write-metadata", () => gulp.task("build-translation-write-metadata", () =>
gulp gulp
.src( .src([
[ path.join(paths.translations_src, "translationMetadata.json"),
path.join(paths.translations_src, "translationMetadata.json"), ...(env.isProdBuild() ? [] : [workDir + "/testMetadata.json"]),
workDir + "/testMetadata.json", workDir + "/translationFingerprints.json",
workDir + "/translationFingerprints.json", ])
],
{ allowEmpty: true }
)
.pipe(merge({})) .pipe(merge({}))
.pipe( .pipe(
transform((data) => { transform((data) => {
@ -415,7 +398,7 @@ gulp.task("build-translation-write-metadata", () =>
gulp.task( gulp.task(
"create-translations", "create-translations",
gulp.series( gulp.series(
...(env.isProdBuild() ? [] : ["create-test-translation"]), gulp.parallel("create-test-metadata", "create-test-translation"),
"build-master-translation", "build-master-translation",
"build-merged-translations", "build-merged-translations",
gulp.parallel(...splitTasks), gulp.parallel(...splitTasks),

View File

@ -1,6 +1,8 @@
const { existsSync } = require("fs"); const { existsSync } = require("fs");
const path = require("path"); const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const { StatsWriterPlugin } = require("webpack-stats-plugin");
const filterStats = require("@bundle-stats/plugin-webpack-filter").default;
const TerserPlugin = require("terser-webpack-plugin"); const 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");
@ -152,6 +154,15 @@ const createWebpackConfig = ({
) )
), ),
!isProdBuild && new LogStartCompilePlugin(), !isProdBuild && new LogStartCompilePlugin(),
isProdBuild &&
new StatsWriterPlugin({
filename: path.relative(
outputPath,
path.join(paths.build_dir, "stats", `${name}.json`)
),
stats: { assets: true, chunks: true, modules: true },
transform: (stats) => JSON.stringify(filterStats(stats)),
}),
].filter(Boolean), ].filter(Boolean),
resolve: { resolve: {
extensions: [".ts", ".js", ".json"], extensions: [".ts", ".js", ".json"],
@ -171,6 +182,8 @@ const createWebpackConfig = ({
"@lit-labs/virtualizer/layouts/grid.js", "@lit-labs/virtualizer/layouts/grid.js",
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver": "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js", "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
"@lit-labs/observers/resize-controller":
"@lit-labs/observers/resize-controller.js",
}, },
}, },
output: { output: {
@ -183,6 +196,7 @@ const createWebpackConfig = ({
isProdBuild && !isStatsBuild ? "[id]-[contenthash].js" : "[name].js", isProdBuild && !isStatsBuild ? "[id]-[contenthash].js" : "[name].js",
assetModuleFilename: assetModuleFilename:
isProdBuild && !isStatsBuild ? "[id]-[contenthash][ext]" : "[id][ext]", isProdBuild && !isStatsBuild ? "[id]-[contenthash][ext]" : "[id][ext]",
crossOriginLoading: "use-credentials",
hashFunction: "xxhash64", hashFunction: "xxhash64",
hashDigest: "base64url", hashDigest: "base64url",
hashDigestLength: 11, // full length of 64 bit base64url hashDigestLength: 11, // full length of 64 bit base64url

View File

@ -1,4 +1,4 @@
import "../../../src/resources/safari-14-attachshadow-patch"; import "../../../src/resources/safari-14-attachshadow-patch";
import "../../../src/resources/ha-style";
import "../../../src/resources/roboto";
import "./layout/hc-connect"; import "./layout/hc-connect";
import("../../../src/resources/ha-style");

View File

@ -100,7 +100,9 @@ export class HcMain extends HassElement {
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
import("../second-load"); import("./hc-lovelace");
import("../../../../src/resources/ha-style");
window.addEventListener("location-changed", () => { window.addEventListener("location-changed", () => {
const panelPath = `/${this._urlPath || "lovelace"}/`; const panelPath = `/${this._urlPath || "lovelace"}/`;
if (location.pathname.startsWith(panelPath)) { if (location.pathname.startsWith(panelPath)) {
@ -260,7 +262,6 @@ export class HcMain extends HassElement {
{ {
strategy: { strategy: {
type: "energy", type: "energy",
show_date_selection: true,
}, },
}, },
], ],
@ -308,7 +309,7 @@ export class HcMain extends HassElement {
? await fetchResources(this.hass!.connection) ? await fetchResources(this.hass!.connection)
: (this._lovelaceConfig as LegacyLovelaceConfig).resources; : (this._lovelaceConfig as LegacyLovelaceConfig).resources;
if (resources) { if (resources) {
loadLovelaceResources(resources, this.hass!.auth.data.hassUrl); loadLovelaceResources(resources, this.hass!);
} }
} }
@ -324,8 +325,7 @@ export class HcMain extends HassElement {
{ {
type: DEFAULT_STRATEGY, type: DEFAULT_STRATEGY,
}, },
this.hass!, this.hass!
{ narrow: false }
) )
); );
} }

View File

@ -1,3 +0,0 @@
import "../../../src/resources/ha-style";
import "../../../src/resources/roboto";
import "./layout/hc-lovelace";

View File

@ -3,6 +3,15 @@ import { DemoConfig } from "../types";
export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) => export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
convertEntities({ convertEntities({
"todo.shopping_list": {
entity_id: "todo.shopping_list",
state: "2",
attributes: {
supported_features: 15,
friendly_name: "Shopping List",
icon: "mdi:cart",
},
},
"zone.home": { "zone.home": {
entity_id: "zone.home", entity_id: "zone.home",
state: "zoning", state: "zoning",

View File

@ -3,6 +3,15 @@ import { DemoConfig } from "../types";
export const demoEntitiesJimpower: DemoConfig["entities"] = () => export const demoEntitiesJimpower: DemoConfig["entities"] = () =>
convertEntities({ convertEntities({
"todo.shopping_list": {
entity_id: "todo.shopping_list",
state: "2",
attributes: {
supported_features: 15,
friendly_name: "Shopping List",
icon: "mdi:cart",
},
},
"zone.powertec": { "zone.powertec": {
entity_id: "zone.powertec", entity_id: "zone.powertec",
state: "zoning", state: "zoning",

View File

@ -4,16 +4,11 @@ export const demoThemeJimpower = () => ({
"primary-color": "#5294E2", "primary-color": "#5294E2",
"label-badge-red": "var(--accent-color)", "label-badge-red": "var(--accent-color)",
"paper-tabs-selection-bar-color": "green", "paper-tabs-selection-bar-color": "green",
"paper-slider-knob-color": "var(--accent-color)",
"light-primary-color": "var(--accent-color)", "light-primary-color": "var(--accent-color)",
"primary-background-color": "#383C45", "primary-background-color": "#383C45",
"primary-text-color": "#FFFFFF", "primary-text-color": "#FFFFFF",
"paper-item-selected_-_background-color": "#434954", "paper-item-selected_-_background-color": "#434954",
"paper-slider-active-color": "var(--accent-color)",
"secondary-background-color": "#383C45", "secondary-background-color": "#383C45",
"paper-slider-container-color":
"linear-gradient(var(--primary-background-color), var(--secondary-background-color)) no-repeat",
"paper-slider-disabled-active-color": "var(--disabled-text-color)",
"disabled-text-color": "#7F848E", "disabled-text-color": "#7F848E",
"paper-item-icon_-_color": "green", "paper-item-icon_-_color": "green",
"paper-grey-200": "#414A59", "paper-grey-200": "#414A59",
@ -32,14 +27,10 @@ export const demoThemeJimpower = () => ({
"switch-unchecked-button-color": "var(--disabled-text-color)", "switch-unchecked-button-color": "var(--disabled-text-color)",
"label-badge-border-color": "green", "label-badge-border-color": "green",
"paper-listbox-color": "var(--primary-color)", "paper-listbox-color": "var(--primary-color)",
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
"card-background-color": "#434954", "card-background-color": "#434954",
"label-badge-text-color": "var(--primary-text-color)", "label-badge-text-color": "var(--primary-text-color)",
"paper-slider-knob-start-color": "var(--accent-color)",
"switch-unchecked-track-color": "var(--disabled-text-color)", "switch-unchecked-track-color": "var(--disabled-text-color)",
"dark-primary-color": "var(--accent-color)", "dark-primary-color": "var(--accent-color)",
"paper-slider-secondary-color": "var(--secondary-background-color)",
"paper-slider-pin-color": "var(--accent-color)",
"paper-item-icon-active-color": "#F9C536", "paper-item-icon-active-color": "#F9C536",
"accent-color": "#E45E65", "accent-color": "#E45E65",
"table-row-alternative-background-color": "#3E424B", "table-row-alternative-background-color": "#3E424B",

View File

@ -3,6 +3,15 @@ import { DemoConfig } from "../types";
export const demoEntitiesKernehed: DemoConfig["entities"] = () => export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
convertEntities({ convertEntities({
"todo.shopping_list": {
entity_id: "todo.shopping_list",
state: "2",
attributes: {
supported_features: 15,
friendly_name: "Shopping List",
icon: "mdi:cart",
},
},
"zone.anna": { "zone.anna": {
entity_id: "zone.anna", entity_id: "zone.anna",
state: "zoning", state: "zoning",

View File

@ -5,17 +5,12 @@ export const demoThemeKernehed = () => ({
"primary-color": "#2980b9", "primary-color": "#2980b9",
"label-badge-red": "var(--accent-color)", "label-badge-red": "var(--accent-color)",
"paper-tabs-selection-bar-color": "green", "paper-tabs-selection-bar-color": "green",
"paper-slider-knob-color": "var(--accent-color)",
"primary-text-color": "#FFFFFF", "primary-text-color": "#FFFFFF",
"light-primary-color": "var(--accent-color)", "light-primary-color": "var(--accent-color)",
"primary-background-color": "#222222", "primary-background-color": "#222222",
"sidebar-icon-color": "#777777", "sidebar-icon-color": "#777777",
"paper-item-selected_-_background-color": "#292929", "paper-item-selected_-_background-color": "#292929",
"paper-slider-active-color": "var(--accent-color)",
"secondary-background-color": "#222222", "secondary-background-color": "#222222",
"paper-slider-container-color":
"linear-gradient(var(--primary-background-color), var(--secondary-background-color)) no-repeat",
"paper-slider-disabled-active-color": "var(--disabled-text-color)",
"disabled-text-color": "#777777", "disabled-text-color": "#777777",
"paper-item-icon_-_color": "green", "paper-item-icon_-_color": "green",
"paper-grey-200": "#222222", "paper-grey-200": "#222222",
@ -33,14 +28,10 @@ export const demoThemeKernehed = () => ({
"switch-unchecked-button-color": "var(--disabled-text-color)", "switch-unchecked-button-color": "var(--disabled-text-color)",
"label-badge-border-color": "green", "label-badge-border-color": "green",
"paper-listbox-color": "#777777", "paper-listbox-color": "#777777",
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
"card-background-color": "#292929", "card-background-color": "#292929",
"label-badge-text-color": "var(--primary-text-color)", "label-badge-text-color": "var(--primary-text-color)",
"paper-slider-knob-start-color": "var(--accent-color)",
"switch-unchecked-track-color": "var(--disabled-text-color)", "switch-unchecked-track-color": "var(--disabled-text-color)",
"dark-primary-color": "var(--accent-color)", "dark-primary-color": "var(--accent-color)",
"paper-slider-secondary-color": "var(--secondary-background-color)",
"paper-slider-pin-color": "var(--accent-color)",
"paper-item-icon-active-color": "#b58e31", "paper-item-icon-active-color": "#b58e31",
"accent-color": "#2980b9", "accent-color": "#2980b9",
"table-row-alternative-background-color": "#292929", "table-row-alternative-background-color": "#292929",

View File

@ -3,6 +3,15 @@ import { DemoConfig } from "../types";
export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () => export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
convertEntities({ convertEntities({
"todo.shopping_list": {
entity_id: "todo.shopping_list",
state: "2",
attributes: {
supported_features: 15,
friendly_name: "Shopping List",
icon: "mdi:cart",
},
},
"sensor.pollen_grabo": { "sensor.pollen_grabo": {
entity_id: "sensor.pollen_grabo", entity_id: "sensor.pollen_grabo",
state: "", state: "",

View File

@ -220,7 +220,8 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
state_filter: ["on"], state_filter: ["on"],
}, },
{ {
type: "shopping-list", type: "todo-list",
entity: "todo.shopping_list",
}, },
{ {
entities: [ entities: [

View File

@ -1,6 +1,5 @@
export const demoThemeTeachingbirds = () => ({ export const demoThemeTeachingbirds = () => ({
"paper-card-header-color": "var(--paper-item-icon-color)", "paper-card-header-color": "var(--paper-item-icon-color)",
"paper-slider-pin-color": "var(--primary-color)",
"paper-listbox-background-color": "#202020", "paper-listbox-background-color": "#202020",
"paper-grey-50": "var(--primary-text-color)", "paper-grey-50": "var(--primary-text-color)",
"paper-item-icon-color": "#d3d3d3", "paper-item-icon-color": "#d3d3d3",
@ -8,8 +7,6 @@ export const demoThemeTeachingbirds = () => ({
"primary-color": "#389638", "primary-color": "#389638",
"light-primary-color": "#6f956f", "light-primary-color": "#6f956f",
"label-badge-red": "var(--primary-color)", "label-badge-red": "var(--primary-color)",
"paper-slider-secondary-color": "var(--light-primary-color)",
"paper-slider-knob-color": "var(--primary-color)",
"paper-listbox-color": "#FFFFFF", "paper-listbox-color": "#FFFFFF",
"paper-toggle-button-checked-bar-color": "var(--light-primary-color)", "paper-toggle-button-checked-bar-color": "var(--light-primary-color)",
"switch-unchecked-track-color": "var(--primary-text-color)", "switch-unchecked-track-color": "var(--primary-text-color)",
@ -17,9 +14,7 @@ export const demoThemeTeachingbirds = () => ({
"label-badge-text-color": "var(--text-primary-color)", "label-badge-text-color": "var(--text-primary-color)",
"primary-background-color": "#303030", "primary-background-color": "#303030",
"sidebar-icon-color": "var(--paper-item-icon-color)", "sidebar-icon-color": "var(--paper-item-icon-color)",
"paper-slider-active-color": "#d8bf50",
"secondary-background-color": "#2b2b2b", "secondary-background-color": "#2b2b2b",
"paper-slider-knob-start-color": "var(--primary-color)",
"paper-item-icon-active-color": "#d8bf50", "paper-item-icon-active-color": "#d8bf50",
"switch-checked-color": "var(--primary-color)", "switch-checked-color": "var(--primary-color)",
"secondary-text-color": "#389638", "secondary-text-color": "#389638",

View File

@ -1,4 +1,4 @@
import "../../src/resources/ha-style";
import "../../src/resources/roboto";
import "../../src/resources/safari-14-attachshadow-patch"; import "../../src/resources/safari-14-attachshadow-patch";
import "./ha-demo"; import "./ha-demo";
import("../../src/resources/ha-style");

View File

@ -22,7 +22,7 @@ import { mockLovelace } from "./stubs/lovelace";
import { mockMediaPlayer } from "./stubs/media_player"; import { mockMediaPlayer } from "./stubs/media_player";
import { mockPersistentNotification } from "./stubs/persistent_notification"; import { mockPersistentNotification } from "./stubs/persistent_notification";
import { mockRecorder } from "./stubs/recorder"; import { mockRecorder } from "./stubs/recorder";
import { mockShoppingList } from "./stubs/shopping_list"; import { mockTodo } from "./stubs/todo";
import { mockSystemLog } from "./stubs/system_log"; import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template"; import { mockTemplate } from "./stubs/template";
import { mockTranslations } from "./stubs/translations"; import { mockTranslations } from "./stubs/translations";
@ -49,7 +49,7 @@ export class HaDemo extends HomeAssistantAppEl {
mockTranslations(hass); mockTranslations(hass);
mockHistory(hass); mockHistory(hass);
mockRecorder(hass); mockRecorder(hass);
mockShoppingList(hass); mockTodo(hass);
mockSystemLog(hass); mockSystemLog(hass);
mockTemplate(hass); mockTemplate(hass);
mockEvents(hass); mockEvents(hass);

View File

@ -1,44 +0,0 @@
import { ShoppingListItem } from "../../../src/data/shopping-list";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
let items: ShoppingListItem[] = [
{
id: 12,
name: "Milk",
complete: false,
},
{
id: 13,
name: "Eggs",
complete: false,
},
{
id: 14,
name: "Oranges",
complete: true,
},
];
export const mockShoppingList = (hass: MockHomeAssistant) => {
hass.mockWS("shopping_list/items", () => items);
hass.mockWS("shopping_list/items/add", (msg) => {
const item: ShoppingListItem = {
id: new Date().getTime(),
complete: false,
name: msg.name,
};
items.push(item);
hass.mockEvent("shopping_list_updated");
return item;
});
hass.mockWS("shopping_list/items/update", ({ type, item_id, ...updates }) => {
items = items.map((item) =>
item.id === item_id ? { ...item, ...updates } : item
);
hass.mockEvent("shopping_list_updated");
});
hass.mockWS("shopping_list/items/clear", () => {
items = items.filter((item) => !item.complete);
hass.mockEvent("shopping_list_updated");
});
};

24
demo/src/stubs/todo.ts Normal file
View File

@ -0,0 +1,24 @@
import { TodoItem, TodoItemStatus } from "../../../src/data/todo";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockTodo = (hass: MockHomeAssistant) => {
hass.mockWS("todo/item/list", () => ({
items: [
{
uid: "12",
summary: "Milk",
status: TodoItemStatus.NeedsAction,
},
{
uid: "13",
summary: "Eggs",
status: TodoItemStatus.NeedsAction,
},
{
uid: "14",
summary: "Oranges",
status: TodoItemStatus.Completed,
},
] as TodoItem[],
}));
};

View File

@ -1,5 +1,5 @@
import "../../src/resources/ha-style";
import "../../src/resources/roboto";
import "./ha-gallery"; import "./ha-gallery";
import("../../src/resources/ha-style");
document.body.appendChild(document.createElement("ha-gallery")); document.body.appendChild(document.createElement("ha-gallery"));

View File

@ -49,11 +49,11 @@ export class DemoHaCircularSlider extends LitElement {
<div class="field"> <div class="field">
<p>Current</p> <p>Current</p>
<ha-slider <ha-slider
labeled
min="10" min="10"
max="30" max="30"
.value=${this.current} .value=${this.current}
@change=${this._currentChanged} @change=${this._currentChanged}
pin
></ha-slider> ></ha-slider>
<p>${this.current} °C</p> <p>${this.current} °C</p>
</div> </div>

View File

@ -57,6 +57,7 @@ const DEVICES = [
sw_version: null, sw_version: null,
hw_version: null, hw_version: null,
via_device_id: null, via_device_id: null,
serial_number: null,
}, },
{ {
area_id: "backyard", area_id: "backyard",
@ -74,6 +75,7 @@ const DEVICES = [
sw_version: null, sw_version: null,
hw_version: null, hw_version: null,
via_device_id: null, via_device_id: null,
serial_number: null,
}, },
{ {
area_id: null, area_id: null,
@ -91,6 +93,7 @@ const DEVICES = [
sw_version: null, sw_version: null,
hw_version: null, hw_version: null,
via_device_id: null, via_device_id: null,
serial_number: null,
}, },
]; ];

View File

@ -57,8 +57,8 @@ export class DemoHaHsColorPicker extends LitElement {
></ha-hs-color-picker> ></ha-hs-color-picker>
<p>Hue : ${this.value[0]}</p> <p>Hue : ${this.value[0]}</p>
<ha-slider <ha-slider
labeled
step="1" step="1"
pin
min="0" min="0"
max="360" max="360"
.value=${this.value[0]} .value=${this.value[0]}
@ -67,8 +67,8 @@ export class DemoHaHsColorPicker extends LitElement {
</ha-slider> </ha-slider>
<p>Saturation : ${this.value[1]}</p> <p>Saturation : ${this.value[1]}</p>
<ha-slider <ha-slider
labeled
step="0.01" step="0.01"
pin
min="0" min="0"
max="1" max="1"
.value=${this.value[1]} .value=${this.value[1]}
@ -77,8 +77,8 @@ export class DemoHaHsColorPicker extends LitElement {
</ha-slider> </ha-slider>
<p>Color Brighness : ${this.brightness}</p> <p>Color Brighness : ${this.brightness}</p>
<ha-slider <ha-slider
labeled
step="1" step="1"
pin
min="0" min="0"
max="255" max="255"
.value=${this.brightness} .value=${this.brightness}

View File

@ -53,6 +53,7 @@ const DEVICES = [
sw_version: null, sw_version: null,
hw_version: null, hw_version: null,
via_device_id: null, via_device_id: null,
serial_number: null,
}, },
{ {
area_id: "backyard", area_id: "backyard",
@ -70,6 +71,7 @@ const DEVICES = [
sw_version: null, sw_version: null,
hw_version: null, hw_version: null,
via_device_id: null, via_device_id: null,
serial_number: null,
}, },
{ {
area_id: null, area_id: null,
@ -87,6 +89,7 @@ const DEVICES = [
sw_version: null, sw_version: null,
hw_version: null, hw_version: null,
via_device_id: null, via_device_id: null,
serial_number: null,
}, },
]; ];

View File

@ -1,3 +0,0 @@
---
title: Shopping List Card
---

View File

@ -0,0 +1,3 @@
---
title: Todo List Card
---

View File

@ -2,25 +2,39 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, query } from "lit/decorators"; import { customElement, query } from "lit/decorators";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { getEntity } from "../../../../src/fake_data/entity";
import { mockTodo } from "../../../../demo/src/stubs/todo";
const ENTITIES = [
getEntity("todo", "shopping_list", "2", {
friendly_name: "Shopping List",
supported_features: 15,
}),
getEntity("todo", "read_only", "2", {
friendly_name: "Read only",
}),
];
const CONFIGS = [ const CONFIGS = [
{ {
heading: "List example", heading: "List example",
config: ` config: `
- type: shopping-list - type: todo-list
entity: todo.shopping_list
`, `,
}, },
{ {
heading: "List with title example", heading: "List with title example",
config: ` config: `
- type: shopping-list - type: todo-list
title: Shopping List title: Shopping List
entity: todo.read_only
`, `,
}, },
]; ];
@customElement("demo-lovelace-shopping-list-card") @customElement("demo-lovelace-todo-list-card")
class DemoShoppingListEntity extends LitElement { class DemoTodoListEntity extends LitElement {
@query("#demos") private _demoRoot!: HTMLElement; @query("#demos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult { protected render(): TemplateResult {
@ -32,18 +46,14 @@ class DemoShoppingListEntity extends LitElement {
const hass = provideHass(this._demoRoot); const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES);
hass.mockAPI("shopping_list", () => [ mockTodo(hass);
{ name: "list", id: 1, complete: false },
{ name: "all", id: 2, complete: false },
{ name: "the", id: 3, complete: false },
{ name: "things", id: 4, complete: true },
]);
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"demo-lovelace-shopping-list-card": DemoShoppingListEntity; "demo-lovelace-todo-list-card": DemoTodoListEntity;
} }
} }

View File

@ -213,6 +213,7 @@ const createDeviceRegistryEntries = (
name: "Tag Reader", name: "Tag Reader",
sw_version: null, sw_version: null,
hw_version: "1.0.0", hw_version: "1.0.0",
serial_number: "00_12_4B_00_22_98_88_7F",
id: "mock-device-id", id: "mock-device-id",
identifiers: [], identifiers: [],
via_device_id: null, via_device_id: null,

View File

@ -1,4 +1,5 @@
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { dump } from "js-yaml";
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 memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -9,7 +10,6 @@ import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-icon-button"; import "../../../../src/components/ha-icon-button";
import "../../../../src/components/search-input"; import "../../../../src/components/search-input";
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware"; import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { dump } from "../../../../src/resources/js-yaml-dump";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware"; import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";

View File

@ -1,15 +1,15 @@
// Compat needs to be first import // Compat needs to be first import
import "../../src/resources/compatibility"; import "../../src/resources/compatibility";
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
import "../../src/resources/roboto";
import "../../src/resources/ha-style";
import "../../src/resources/safari-14-attachshadow-patch"; import "../../src/resources/safari-14-attachshadow-patch";
import "./hassio-main"; import "./hassio-main";
setCancelSyntheticClickEvents(false); import("../../src/resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then(
({ setCancelSyntheticClickEvents }) => setCancelSyntheticClickEvents(false)
);
const styleEl = document.createElement("style"); const styleEl = document.createElement("style");
styleEl.innerHTML = ` styleEl.textContent = `
body { body {
font-family: Roboto, sans-serif; font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;

View File

@ -25,24 +25,24 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.23.1", "@babel/runtime": "7.23.2",
"@braintree/sanitize-url": "6.0.4", "@braintree/sanitize-url": "6.0.4",
"@codemirror/autocomplete": "6.9.1", "@codemirror/autocomplete": "6.10.2",
"@codemirror/commands": "6.3.0", "@codemirror/commands": "6.3.0",
"@codemirror/language": "6.9.1", "@codemirror/language": "6.9.1",
"@codemirror/legacy-modes": "6.3.3", "@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.4", "@codemirror/search": "6.5.4",
"@codemirror/state": "6.2.1", "@codemirror/state": "6.3.1",
"@codemirror/view": "6.21.1", "@codemirror/view": "6.21.3",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.10.3", "@formatjs/intl-datetimeformat": "6.11.0",
"@formatjs/intl-displaynames": "6.5.2", "@formatjs/intl-displaynames": "6.6.0",
"@formatjs/intl-getcanonicallocales": "2.2.1", "@formatjs/intl-getcanonicallocales": "2.3.0",
"@formatjs/intl-listformat": "7.4.2", "@formatjs/intl-listformat": "7.5.0",
"@formatjs/intl-locale": "3.3.4", "@formatjs/intl-locale": "3.4.0",
"@formatjs/intl-numberformat": "8.7.2", "@formatjs/intl-numberformat": "8.8.0",
"@formatjs/intl-pluralrules": "5.2.6", "@formatjs/intl-pluralrules": "5.2.7",
"@formatjs/intl-relativetimeformat": "11.2.6", "@formatjs/intl-relativetimeformat": "11.2.7",
"@fullcalendar/core": "6.1.9", "@fullcalendar/core": "6.1.9",
"@fullcalendar/daygrid": "6.1.9", "@fullcalendar/daygrid": "6.1.9",
"@fullcalendar/interaction": "6.1.9", "@fullcalendar/interaction": "6.1.9",
@ -52,10 +52,12 @@
"@lezer/highlight": "1.1.6", "@lezer/highlight": "1.1.6",
"@lit-labs/context": "0.4.1", "@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.4", "@lit-labs/motion": "1.0.4",
"@lit-labs/observers": "2.0.1",
"@lit-labs/virtualizer": "2.0.7", "@lit-labs/virtualizer": "2.0.7",
"@lrnwebcomponents/simple-tooltip": "7.0.18", "@lrnwebcomponents/simple-tooltip": "7.0.18",
"@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-base": "0.27.0",
"@material/mwc-button": "0.27.0", "@material/mwc-button": "0.27.0",
"@material/mwc-checkbox": "0.27.0", "@material/mwc-checkbox": "0.27.0",
"@material/mwc-circular-progress": "0.27.0", "@material/mwc-circular-progress": "0.27.0",
@ -71,7 +73,6 @@
"@material/mwc-radio": "0.27.0", "@material/mwc-radio": "0.27.0",
"@material/mwc-ripple": "0.27.0", "@material/mwc-ripple": "0.27.0",
"@material/mwc-select": "0.27.0", "@material/mwc-select": "0.27.0",
"@material/mwc-slider": "0.27.0",
"@material/mwc-switch": "0.27.0", "@material/mwc-switch": "0.27.0",
"@material/mwc-tab": "0.27.0", "@material/mwc-tab": "0.27.0",
"@material/mwc-tab-bar": "0.27.0", "@material/mwc-tab-bar": "0.27.0",
@ -80,22 +81,21 @@
"@material/mwc-top-app-bar": "0.27.0", "@material/mwc-top-app-bar": "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", "@material/web": "=1.0.1",
"@mdi/js": "7.2.96", "@mdi/js": "7.3.67",
"@mdi/svg": "7.2.96", "@mdi/svg": "7.3.67",
"@polymer/iron-flex-layout": "3.0.1", "@polymer/iron-flex-layout": "3.0.1",
"@polymer/iron-input": "3.0.1", "@polymer/iron-input": "3.0.1",
"@polymer/iron-resizable-behavior": "3.0.1", "@polymer/iron-resizable-behavior": "3.0.1",
"@polymer/paper-input": "3.2.1", "@polymer/paper-input": "3.2.1",
"@polymer/paper-item": "3.0.1", "@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1", "@polymer/paper-listbox": "3.0.1",
"@polymer/paper-slider": "3.0.1",
"@polymer/paper-tabs": "3.1.0", "@polymer/paper-tabs": "3.1.0",
"@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.10", "@vaadin/combo-box": "24.2.0",
"@vaadin/vaadin-themable-mixin": "24.1.10", "@vaadin/vaadin-themable-mixin": "24.2.0",
"@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",
@ -105,7 +105,7 @@
"app-datepicker": "5.1.1", "app-datepicker": "5.1.1",
"chart.js": "4.4.0", "chart.js": "4.4.0",
"comlink": "4.4.1", "comlink": "4.4.1",
"core-js": "3.32.2", "core-js": "3.33.1",
"cropperjs": "1.6.1", "cropperjs": "1.6.1",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"date-fns-tz": "2.0.0", "date-fns-tz": "2.0.0",
@ -114,15 +114,15 @@
"fuse.js": "6.6.2", "fuse.js": "6.6.2",
"google-timezones-json": "1.2.0", "google-timezones-json": "1.2.0",
"hls.js": "1.4.12", "hls.js": "1.4.12",
"home-assistant-js-websocket": "8.2.0", "home-assistant-js-websocket": "9.1.0",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"intl-messageformat": "10.5.3", "intl-messageformat": "10.5.4",
"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.8.0", "lit": "2.8.0",
"luxon": "3.4.3", "luxon": "3.4.3",
"marked": "9.0.3", "marked": "9.1.2",
"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",
@ -141,7 +141,7 @@
"ua-parser-js": "1.0.36", "ua-parser-js": "1.0.36",
"unfetch": "5.0.0", "unfetch": "5.0.0",
"vis-data": "7.1.7", "vis-data": "7.1.7",
"vis-network": "9.1.6", "vis-network": "9.1.8",
"vue": "2.7.14", "vue": "2.7.14",
"vue2-daterange-picker": "0.6.8", "vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0", "weekstart": "2.0.0",
@ -154,48 +154,49 @@
"xss": "1.0.14" "xss": "1.0.14"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.23.0", "@babel/core": "7.23.2",
"@babel/plugin-proposal-decorators": "7.23.0", "@babel/plugin-proposal-decorators": "7.23.2",
"@babel/plugin-transform-runtime": "7.22.15", "@babel/plugin-transform-runtime": "7.23.2",
"@babel/preset-env": "7.22.20", "@babel/preset-env": "7.23.2",
"@babel/preset-typescript": "7.23.0", "@babel/preset-typescript": "7.23.2",
"@bundle-stats/plugin-webpack-filter": "4.7.7",
"@koa/cors": "4.0.0", "@koa/cors": "4.0.0",
"@lokalise/node-api": "12.0.0", "@lokalise/node-api": "12.0.0",
"@octokit/auth-oauth-device": "6.0.1", "@octokit/auth-oauth-device": "6.0.1",
"@octokit/plugin-retry": "6.0.1", "@octokit/plugin-retry": "6.0.1",
"@octokit/rest": "20.0.2", "@octokit/rest": "20.0.2",
"@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.4",
"@rollup/plugin-commonjs": "25.0.4", "@rollup/plugin-commonjs": "25.0.7",
"@rollup/plugin-json": "6.0.0", "@rollup/plugin-json": "6.0.1",
"@rollup/plugin-node-resolve": "15.2.1", "@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.2", "@rollup/plugin-replace": "5.0.4",
"@types/babel__plugin-transform-runtime": "7.9.3", "@types/babel__plugin-transform-runtime": "7.9.4",
"@types/chromecast-caf-receiver": "6.0.10", "@types/chromecast-caf-receiver": "6.0.11",
"@types/chromecast-caf-sender": "1.0.6", "@types/chromecast-caf-sender": "1.0.7",
"@types/esprima": "4.0.4", "@types/esprima": "4.0.5",
"@types/glob": "8.1.0", "@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.0", "@types/html-minifier-terser": "7.0.1",
"@types/js-yaml": "4.0.6", "@types/js-yaml": "4.0.8",
"@types/leaflet": "1.9.6", "@types/leaflet": "1.9.7",
"@types/leaflet-draw": "1.0.8", "@types/leaflet-draw": "1.0.9",
"@types/luxon": "3.3.2", "@types/luxon": "3.3.3",
"@types/mocha": "10.0.2", "@types/mocha": "10.0.3",
"@types/qrcode": "1.5.2", "@types/qrcode": "1.5.4",
"@types/serve-handler": "6.1.2", "@types/serve-handler": "6.1.3",
"@types/sortablejs": "1.15.3", "@types/sortablejs": "1.15.4",
"@types/tar": "6.1.6", "@types/tar": "6.1.7",
"@types/ua-parser-js": "0.7.37", "@types/ua-parser-js": "0.7.38",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.7.3", "@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.7.3", "@typescript-eslint/parser": "6.8.0",
"@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.10", "chai": "4.3.10",
"del": "7.1.0", "del": "7.1.0",
"eslint": "8.50.0", "eslint": "8.52.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": "9.0.0", "eslint-config-prettier": "9.0.0",
@ -220,10 +221,10 @@
"husky": "8.0.3", "husky": "8.0.3",
"instant-mocha": "1.5.2", "instant-mocha": "1.5.2",
"jszip": "3.10.1", "jszip": "3.10.1",
"lint-staged": "14.0.1", "lint-staged": "15.0.2",
"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.4", "magic-string": "0.30.5",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"mocha": "10.2.0", "mocha": "10.2.0",
"object-hash": "3.0.0", "object-hash": "3.0.0",
@ -235,7 +236,7 @@
"rollup-plugin-terser": "7.0.2", "rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.9.2", "rollup-plugin-visualizer": "5.9.2",
"serve-handler": "6.1.5", "serve-handler": "6.1.5",
"sinon": "16.0.0", "sinon": "17.0.0",
"source-map-url": "0.4.1", "source-map-url": "0.4.1",
"systemjs": "6.14.2", "systemjs": "6.14.2",
"tar": "6.2.0", "tar": "6.2.0",
@ -244,10 +245,11 @@
"typescript": "5.2.2", "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.89.0",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1", "webpack-dev-server": "4.15.1",
"webpack-manifest-plugin": "5.0.0", "webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "5.0.2", "webpackbar": "5.0.2",
"workbox-build": "7.0.0" "workbox-build": "7.0.0"
}, },
@ -255,8 +257,9 @@
"resolutions": { "resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.5.1#./.yarn/patches/@polymer/polymer/pr-5569.patch", "@polymer/polymer": "patch:@polymer/polymer@3.5.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@material/mwc-button@^0.25.3": "^0.27.0", "@material/mwc-button@^0.25.3": "^0.27.0",
"lit@^2.7.4 || ^3.0.0": "^2.7.4",
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch", "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.3" "packageManager": "yarn@3.6.4"
} }

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20231005.0" version = "20231025.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"

View File

@ -33,7 +33,7 @@ fi
docker run \ docker run \
-v ${LOCAL_FILE}:/opt/src/${LOCAL_FILE} \ -v ${LOCAL_FILE}:/opt/src/${LOCAL_FILE} \
lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d lokalise2 \ lokalise/lokalise-cli-2:v2.6.10 lokalise2 \
--token ${LOKALISE_TOKEN} \ --token ${LOKALISE_TOKEN} \
--project-id ${PROJECT_ID} \ --project-id ${PROJECT_ID} \
file upload \ file upload \

View File

@ -26,14 +26,13 @@ export class HaAuthFormString extends HaFormString {
} }
ha-auth-form-string ha-icon-button { ha-auth-form-string ha-icon-button {
position: absolute; position: absolute;
top: 1em; top: 8px;
right: 12px; right: 8px;
--mdc-icon-button-size: 24px;
color: var(--secondary-text-color);
}
ha-auth-form-string ha-icon-button {
inset-inline-start: initial; inset-inline-start: initial;
inset-inline-end: 12px; inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction); direction: var(--direction);
} }
</style> </style>
@ -63,7 +62,7 @@ export class HaAuthFormString extends HaFormString {
.validationMessage=${this.schema.required ? "Required" : undefined} .validationMessage=${this.schema.required ? "Required" : undefined}
@input=${this._valueChanged} @input=${this._valueChanged}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-auth-textfield> ></ha-auth-textfield>
${this.renderIcon()} ${this.renderIcon()}
</ha-auth-textfield> </ha-auth-textfield>
`; `;

View File

@ -0,0 +1,9 @@
export function getAllCombinations<T>(arr: T[]) {
return arr.reduce<T[][]>(
(combinations, element) =>
combinations.concat(
combinations.map((combination) => [...combination, element])
),
[[]]
);
}

View File

@ -16,6 +16,7 @@ import {
mdiCarCoolantLevel, mdiCarCoolantLevel,
mdiCash, mdiCash,
mdiChatSleep, mdiChatSleep,
mdiClipboardList,
mdiClock, mdiClock,
mdiCloudUpload, mdiCloudUpload,
mdiCog, mdiCog,
@ -120,6 +121,7 @@ export const FIXED_DOMAIN_ICONS = {
siren: mdiBullhorn, siren: mdiBullhorn,
stt: mdiMicrophoneMessage, stt: mdiMicrophoneMessage,
text: mdiFormTextbox, text: mdiFormTextbox,
todo: mdiClipboardList,
time: mdiClock, time: mdiClock,
timer: mdiTimerOutline, timer: mdiTimerOutline,
tts: mdiSpeakerMessage, tts: mdiSpeakerMessage,

View File

@ -5,12 +5,15 @@ import { FrontendLocaleData, TimeZone } from "../../data/translation";
const calcZonedDate = ( const calcZonedDate = (
date: Date, date: Date,
tz: string, tz: string,
fn: (date: Date, options?: any) => Date, fn: (date: Date, options?: any) => Date | number | boolean,
options? options?
) => { ) => {
const inputZoned = utcToZonedTime(date, tz); const inputZoned = utcToZonedTime(date, tz);
const fnZoned = fn(inputZoned, options); const fnZoned = fn(inputZoned, options);
return zonedTimeToUtc(fnZoned, tz); if (fnZoned instanceof Date) {
return zonedTimeToUtc(fnZoned, tz) as Date;
}
return fnZoned;
}; };
export const calcDate = ( export const calcDate = (
@ -21,5 +24,16 @@ export const calcDate = (
options? options?
) => ) =>
locale.time_zone === TimeZone.server locale.time_zone === TimeZone.server
? calcZonedDate(date, config.time_zone, fn, options) ? (calcZonedDate(date, config.time_zone, fn, options) as Date)
: fn(date, options);
export const calcDateProperty = (
date: Date,
fn: (date: Date, options?: any) => boolean | number,
locale: FrontendLocaleData,
config: HassConfig,
options?
) =>
locale.time_zone === TimeZone.server
? (calcZonedDate(date, config.time_zone, fn, options) as number | boolean)
: fn(date, options); : fn(date, options);

View File

@ -37,6 +37,23 @@ const formatDateMem = memoizeOne(
}) })
); );
// Aug 10, 2021
export const formatDateShort = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatDateShortMem(locale, config.time_zone).format(dateObj);
const formatDateShortMem = memoizeOne(
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "short",
day: "numeric",
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
})
);
// 10/08/2021 // 10/08/2021
export const formatDateNumeric = ( export const formatDateNumeric = (
dateObj: Date, dateObj: Date,
@ -102,13 +119,13 @@ const formatDateNumericMem = memoizeOne(
); );
// Aug 10 // Aug 10
export const formatDateShort = ( export const formatDateVeryShort = (
dateObj: Date, dateObj: Date,
locale: FrontendLocaleData, locale: FrontendLocaleData,
config: HassConfig config: HassConfig
) => formatDateShortMem(locale, config.time_zone).format(dateObj); ) => formatDateVeryShortMem(locale, config.time_zone).format(dateObj);
const formatDateShortMem = memoizeOne( const formatDateVeryShortMem = memoizeOne(
(locale: FrontendLocaleData, serverTimeZone: string) => (locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, { new Intl.DateTimeFormat(locale.language, {
day: "numeric", day: "numeric",

View File

@ -1,8 +1,13 @@
import { HaDurationData } from "../../components/ha-duration-input"; import { HaDurationData } from "../../components/ha-duration-input";
import { FrontendLocaleData } from "../../data/translation";
import "../../resources/intl-polyfill";
const leftPad = (num: number) => (num < 10 ? `0${num}` : num); const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
export const formatDuration = (duration: HaDurationData) => { export const formatDuration = (
locale: FrontendLocaleData,
duration: HaDurationData
) => {
const d = duration.days || 0; const d = duration.days || 0;
const h = duration.hours || 0; const h = duration.hours || 0;
const m = duration.minutes || 0; const m = duration.minutes || 0;
@ -10,7 +15,11 @@ export const formatDuration = (duration: HaDurationData) => {
const ms = duration.milliseconds || 0; const ms = duration.milliseconds || 0;
if (d > 0) { if (d > 0) {
return `${d} day${d === 1 ? "" : "s"} ${h}:${leftPad(m)}:${leftPad(s)}`; return `${Intl.NumberFormat(locale.language, {
style: "unit",
unit: "day",
unitDisplay: "long",
}).format(d)} ${h}:${leftPad(m)}:${leftPad(s)}`;
} }
if (h > 0) { if (h > 0) {
return `${h}:${leftPad(m)}:${leftPad(s)}`; return `${h}:${leftPad(m)}:${leftPad(s)}`;
@ -19,10 +28,18 @@ export const formatDuration = (duration: HaDurationData) => {
return `${m}:${leftPad(s)}`; return `${m}:${leftPad(s)}`;
} }
if (s > 0) { if (s > 0) {
return `${s} second${s === 1 ? "" : "s"}`; return Intl.NumberFormat(locale.language, {
style: "unit",
unit: "second",
unitDisplay: "long",
}).format(s);
} }
if (ms > 0) { if (ms > 0) {
return `${ms} millisecond${ms === 1 ? "" : "s"}`; return Intl.NumberFormat(locale.language, {
style: "unit",
unit: "millisecond",
unitDisplay: "long",
}).format(ms);
} }
return null; return null;
}; };

View File

@ -41,9 +41,7 @@ export const applyThemesOnElement = (
// If there is no explicitly desired dark mode provided, we automatically // If there is no explicitly desired dark mode provided, we automatically
// use the active one from `themes`. // use the active one from `themes`.
const darkMode = const darkMode =
themeSettings && themeSettings?.dark !== undefined themeSettings?.dark !== undefined ? themeSettings.dark : themes.darkMode;
? themeSettings?.dark
: themes.darkMode;
let cacheKey = themeToApply; let cacheKey = themeToApply;
let themeRules: Partial<ThemeVars> = {}; let themeRules: Partial<ThemeVars> = {};
@ -135,10 +133,19 @@ export const applyThemesOnElement = (
// Set and/or reset styles // Set and/or reset styles
if (element.updateStyles) { if (element.updateStyles) {
// Use updateStyles() method of Polymer elements
element.updateStyles(styles); element.updateStyles(styles);
} else if (window.ShadyCSS) { } else if (window.ShadyCSS) {
// Implement updateStyles() method of Polymer elements // Use ShadyCSS if available
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles); window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
} else {
for (const s in styles) {
if (s === null) {
element.style.removeProperty(s);
} else {
element.style.setProperty(s, styles[s]);
}
}
} }
}; };

View File

@ -1,19 +1,29 @@
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1 // https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
export const slugify = (value: string, delimiter = "_") => { export const slugify = (value: string, delimiter = "_") => {
const a = const a =
"àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;"; "àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·";
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`; const b = `aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}`;
const p = new RegExp(a.split("").join("|"), "g"); const p = new RegExp(a.split("").join("|"), "g");
return value let slugified;
.toString()
.toLowerCase() if (value === "") {
.replace(/\s+/g, delimiter) // Replace spaces with delimiter slugified = "";
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters } else {
.replace(/&/g, `${delimiter}and${delimiter}`) // Replace & with 'and' slugified = value
.replace(/[^\w-]+/g, "") // Remove all non-word characters .toString()
.replace(/-/g, delimiter) // Replace - with delimiter .toLowerCase()
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter .replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
.replace(new RegExp(`^${delimiter}+`), "") // Trim delimiter from start of text .replace(/(?<=\d),(?=\d)/g, "") // Remove Commas between numbers
.replace(new RegExp(`${delimiter}+$`), ""); // Trim delimiter from end of text .replace(/[^a-z0-9]+/g, delimiter) // Replace all non-word characters
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
.replace(new RegExp(`^${delimiter}+`), "") // Trim delimiter from start of text
.replace(new RegExp(`${delimiter}+$`), ""); // Trim delimiter from end of text
if (slugified === "") {
slugified = "unknown";
}
}
return slugified;
}; };

View File

@ -13,45 +13,33 @@ export const handleStructError = (
for (const failure of err.failures()) { for (const failure of err.failures()) {
if (failure.value === undefined) { if (failure.value === undefined) {
errors.push( errors.push(
hass.localize( hass.localize("ui.errors.config.key_missing", {
"ui.errors.config.key_missing", key: failure.path.join("."),
"key", })
failure.path.join(".")
)
); );
} else if (failure.type === "never") { } else if (failure.type === "never") {
warnings.push( warnings.push(
hass.localize( hass.localize("ui.errors.config.key_not_expected", {
"ui.errors.config.key_not_expected", key: failure.path.join("."),
"key", })
failure.path.join(".")
)
); );
} else if (failure.type === "union") { } else if (failure.type === "union") {
continue; continue;
} else if (failure.type === "enums") { } else if (failure.type === "enums") {
warnings.push( warnings.push(
hass.localize( hass.localize("ui.errors.config.key_wrong_type", {
"ui.errors.config.key_wrong_type", key: failure.path.join("."),
"key", type_correct: failure.message.replace("Expected ", "").split(", ")[0],
failure.path.join("."), type_wrong: JSON.stringify(failure.value),
"type_correct", })
failure.message.replace("Expected ", "").split(", ")[0],
"type_wrong",
JSON.stringify(failure.value)
)
); );
} else { } else {
warnings.push( warnings.push(
hass.localize( hass.localize("ui.errors.config.key_wrong_type", {
"ui.errors.config.key_wrong_type", key: failure.path.join("."),
"key", type_correct: failure.refinement || failure.type,
failure.path.join("."), type_wrong: JSON.stringify(failure.value),
"type_correct", })
failure.refinement || failure.type,
"type_wrong",
JSON.stringify(failure.value)
)
); );
} }
} }

View File

@ -1,6 +1,13 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js"; import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../ha-circular-progress"; import "../ha-circular-progress";
import "../ha-svg-icon"; import "../ha-svg-icon";
@ -27,7 +34,7 @@ export class HaProgressButton extends LitElement {
<slot></slot> <slot></slot>
</mwc-button> </mwc-button>
${!overlay ${!overlay
? "" ? nothing
: html` : html`
<div class="progress"> <div class="progress">
${this._result === "success" ${this._result === "success"

View File

@ -39,7 +39,7 @@ import {
formatDate, formatDate,
formatDateMonth, formatDateMonth,
formatDateMonthYear, formatDateMonthYear,
formatDateShort, formatDateVeryShort,
formatDateWeekdayDay, formatDateWeekdayDay,
formatDateYear, formatDateYear,
} from "../../common/datetime/format_date"; } from "../../common/datetime/format_date";
@ -128,7 +128,7 @@ _adapters._date.override({
this.options.config this.options.config
); );
case "day": case "day":
return formatDateShort( return formatDateVeryShort(
new Date(time), new Date(time),
this.options.locale, this.options.locale,
this.options.config this.options.config

View File

@ -12,6 +12,7 @@ import { styleMap } from "lit/directives/style-map";
import { clamp } from "../../common/number/clamp"; import { clamp } from "../../common/number/clamp";
import { computeRTL } from "../../common/util/compute_rtl"; import { computeRTL } from "../../common/util/compute_rtl";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { debounce } from "../../common/util/debounce";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
@ -52,6 +53,12 @@ export class HaChartBase extends LitElement {
@state() private _hiddenDatasets: Set<number> = new Set(); @state() private _hiddenDatasets: Set<number> = new Set();
private _paddingUpdateCount = 0;
private _paddingUpdateLock = false;
private _paddingYAxisInternal = 0;
public disconnectedCallback() { public disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
this._releaseCanvas(); this._releaseCanvas();
@ -104,9 +111,44 @@ export class HaChartBase extends LitElement {
}); });
} }
public shouldUpdate(changedProps: PropertyValues): boolean {
if (
this._paddingUpdateLock &&
changedProps.size === 1 &&
changedProps.has("paddingYAxis")
) {
return false;
}
return true;
}
private _debouncedClearUpdates = debounce(
() => {
this._paddingUpdateCount = 0;
},
2000,
false
);
public willUpdate(changedProps: PropertyValues): void { public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps); super.willUpdate(changedProps);
if (!this._paddingUpdateLock) {
this._paddingYAxisInternal = this.paddingYAxis;
if (changedProps.size === 1 && changedProps.has("paddingYAxis")) {
this._paddingUpdateCount++;
if (this._paddingUpdateCount > 300) {
this._paddingUpdateLock = true;
// eslint-disable-next-line
console.error(
"Detected excessive chart padding updates, possibly an infinite loop. Disabling axis padding."
);
} else {
this._debouncedClearUpdates();
}
}
}
if (!this.hasUpdated || !this.chart) { if (!this.hasUpdated || !this.chart) {
return; return;
} }
@ -171,10 +213,10 @@ export class HaChartBase extends LitElement {
this.height ?? this._chartHeight ?? this.clientWidth / 2 this.height ?? this._chartHeight ?? this.clientWidth / 2
}px`, }px`,
"padding-left": `${ "padding-left": `${
computeRTL(this.hass) ? 0 : this.paddingYAxis computeRTL(this.hass) ? 0 : this._paddingYAxisInternal
}px`, }px`,
"padding-right": `${ "padding-right": `${
computeRTL(this.hass) ? this.paddingYAxis : 0 computeRTL(this.hass) ? this._paddingYAxisInternal : 0
}px`, }px`,
})} })}
> >
@ -324,7 +366,7 @@ export class HaChartBase extends LitElement {
clamp( clamp(
context.tooltip.caretX, context.tooltip.caretX,
100, 100,
this.clientWidth - 100 - this.paddingYAxis this.clientWidth - 100 - this._paddingYAxisInternal
) - ) -
100 + 100 +
"px", "px",

View File

@ -141,16 +141,10 @@ export class StateHistoryChartLine extends LitElement {
`${context.dataset.label}: ${formatNumber( `${context.dataset.label}: ${formatNumber(
context.parsed.y, context.parsed.y,
this.hass.locale, this.hass.locale,
this.data[context.datasetIndex]?.entity_id getNumberFormatOptions(
? getNumberFormatOptions( undefined,
this.hass.states[ this.hass.entities[this._entityIds[context.datasetIndex]]
this.data[context.datasetIndex].entity_id )
],
this.hass.entities[
this.data[context.datasetIndex].entity_id
]
)
: undefined
)} ${this.unit}`, )} ${this.unit}`,
}, },
}, },

View File

@ -73,9 +73,9 @@ export class StateHistoryCharts extends LitElement {
@property({ type: Boolean }) public isLoadingData = false; @property({ type: Boolean }) public isLoadingData = false;
@state() private _computedStartTime!: Date; private _computedStartTime!: Date;
@state() private _computedEndTime!: Date; private _computedEndTime!: Date;
@state() private _maxYWidth = 0; @state() private _maxYWidth = 0;
@ -114,31 +114,6 @@ export class StateHistoryCharts extends LitElement {
${this.hass.localize("ui.components.history_charts.no_history_found")} ${this.hass.localize("ui.components.history_charts.no_history_found")}
</div>`; </div>`;
} }
const now = new Date();
this._computedEndTime =
this.upToNow || !this.endTime || this.endTime > now ? now : this.endTime;
if (this.startTime) {
this._computedStartTime = this.startTime;
} else if (this.hoursToShow) {
this._computedStartTime = new Date(
new Date().getTime() - 60 * 60 * this.hoursToShow * 1000
);
} else {
this._computedStartTime = new Date(
this.historyData.timeline.reduce(
(minTime, stateInfo) =>
Math.min(
minTime,
new Date(stateInfo.data[0].last_changed).getTime()
),
new Date().getTime()
)
);
}
const combinedItems = this.historyData.timeline.length const combinedItems = this.historyData.timeline.length
? (this.virtualize ? (this.virtualize
? chunkData(this.historyData.timeline, CANVAS_TIMELINE_ROWS_CHUNK) ? chunkData(this.historyData.timeline, CANVAS_TIMELINE_ROWS_CHUNK)
@ -220,10 +195,45 @@ export class StateHistoryCharts extends LitElement {
return true; return true;
} }
protected willUpdate() { protected willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) { if (!this.hasUpdated) {
loadVirtualizer(); loadVirtualizer();
} }
if (
[...changedProps.keys()].some(
(prop) =>
!(
["_maxYWidth", "_childYWidths", "_chartCount"] as PropertyKey[]
).includes(prop)
)
) {
// Don't recompute times when we just want to update layout
const now = new Date();
this._computedEndTime =
this.upToNow || !this.endTime || this.endTime > now
? now
: this.endTime;
if (this.startTime) {
this._computedStartTime = this.startTime;
} else if (this.hoursToShow) {
this._computedStartTime = new Date(
new Date().getTime() - 60 * 60 * this.hoursToShow * 1000
);
} else {
this._computedStartTime = new Date(
this.historyData.timeline.reduce(
(minTime, stateInfo) =>
Math.min(
minTime,
new Date(stateInfo.data[0].last_changed).getTime()
),
new Date().getTime()
)
);
}
}
} }
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {

View File

@ -19,6 +19,7 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { import {
formatNumber, formatNumber,
numberFormatToLocale, numberFormatToLocale,
getNumberFormatOptions,
} from "../../common/number/format_number"; } from "../../common/number/format_number";
import { import {
getDisplayUnit, getDisplayUnit,
@ -74,6 +75,8 @@ export class StatisticsChart extends LitElement {
@state() private _chartData: ChartData = { datasets: [] }; @state() private _chartData: ChartData = { datasets: [] };
@state() private _statisticIds: string[] = [];
@state() private _chartOptions?: ChartOptions; @state() private _chartOptions?: ChartOptions;
@query("ha-chart-base") private _chart?: HaChartBase; @query("ha-chart-base") private _chart?: HaChartBase;
@ -189,7 +192,11 @@ export class StatisticsChart 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(
undefined,
this.hass.entities[this._statisticIds[context.datasetIndex]]
)
)} ${ )} ${
// @ts-ignore // @ts-ignore
context.dataset.unit || "" context.dataset.unit || ""
@ -248,6 +255,7 @@ export class StatisticsChart extends LitElement {
let colorIndex = 0; let colorIndex = 0;
const statisticsData = Object.entries(this.statisticsData); const statisticsData = Object.entries(this.statisticsData);
const totalDataSets: ChartDataset<"line">[] = []; const totalDataSets: ChartDataset<"line">[] = [];
const statisticIds: string[] = [];
let endTime: Date; let endTime: Date;
if (statisticsData.length === 0) { if (statisticsData.length === 0) {
@ -386,6 +394,7 @@ export class StatisticsChart extends LitElement {
unit: meta?.unit_of_measurement, unit: meta?.unit_of_measurement,
band, band,
}); });
statisticIds.push(statistic_id);
} }
}); });
@ -411,11 +420,7 @@ export class StatisticsChart extends LitElement {
} else { } else {
val = stat[type]; val = stat[type];
} }
dataValues.push( dataValues.push(val ?? null);
val !== null && val !== undefined
? Math.round(val * 100) / 100
: null
);
}); });
pushData(startDate, new Date(stat.end), dataValues); pushData(startDate, new Date(stat.end), dataValues);
}); });
@ -431,6 +436,7 @@ export class StatisticsChart extends LitElement {
this._chartData = { this._chartData = {
datasets: totalDataSets, datasets: totalDataSets,
}; };
this._statisticIds = statisticIds;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -31,6 +31,10 @@ const Component = Vue.extend({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
openingDirection: {
type: String,
default: "right",
},
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -66,7 +70,7 @@ const Component = Vue.extend({
props: { props: {
"time-picker": this.timePicker, "time-picker": this.timePicker,
"auto-apply": this.autoApply, "auto-apply": this.autoApply,
opens: "right", opens: this.openingDirection,
"show-dropdowns": false, "show-dropdowns": false,
"time-picker24-hour": this.twentyfourHours, "time-picker24-hour": this.twentyfourHours,
disabled: this.disabled, disabled: this.disabled,
@ -126,9 +130,9 @@ class DateRangePickerElement extends WrappedElement {
${dateRangePickerStyles} ${dateRangePickerStyles}
.calendars { .calendars {
display: flex; display: flex;
flex-wrap: nowrap !important;
} }
.daterangepicker { .daterangepicker {
left: 0px !important;
top: auto; top: auto;
box-shadow: var(--ha-card-box-shadow, none); box-shadow: var(--ha-card-box-shadow, none);
background-color: var(--card-background-color); background-color: var(--card-background-color);
@ -252,6 +256,10 @@ class DateRangePickerElement extends WrappedElement {
direction: ltr; direction: ltr;
text-align: left; text-align: left;
} }
.vue-daterange-picker{
min-width: unset !important;
display: block !important;
}
`; `;
const shadowRoot = this.shadowRoot!; const shadowRoot = this.shadowRoot!;
shadowRoot.appendChild(style); shadowRoot.appendChild(style);

View File

@ -87,6 +87,8 @@ export class HaStatisticPicker extends LitElement {
@property({ type: Array, attribute: "exclude-statistics" }) @property({ type: Array, attribute: "exclude-statistics" })
public excludeStatistics?: string[]; public excludeStatistics?: string[];
@property() public helpMissingEntityUrl = "/more-info/statistics/";
@state() private _opened?: boolean; @state() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox; @query("ha-combo-box", true) public comboBox!: HaComboBox;
@ -111,7 +113,7 @@ export class HaStatisticPicker extends LitElement {
? html`<a ? html`<a
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
href=${documentationUrl(this.hass, "/more-info/statistics/")} href=${documentationUrl(this.hass, this.helpMissingEntityUrl)}
>${this.hass.localize( >${this.hass.localize(
"ui.components.statistic-picker.learn_more" "ui.components.statistic-picker.learn_more"
)}</a )}</a

View File

@ -112,9 +112,7 @@ export class StateBadge extends LitElement {
const stateObj = this.stateObj; const stateObj = this.stateObj;
const iconStyle: { [name: string]: string } = {}; const iconStyle: { [name: string]: string } = {};
const hostStyle: Partial<CSSStyleDeclaration> = { let backgroundImage = "";
backgroundImage: "",
};
this._showIcon = true; this._showIcon = true;
@ -135,10 +133,12 @@ export class StateBadge extends LitElement {
if (domain === "camera") { if (domain === "camera") {
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80); imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
} }
hostStyle.backgroundImage = `url(${imageUrl})`; backgroundImage = `url(${imageUrl})`;
this._showIcon = false; this._showIcon = false;
if (domain === "update") { if (domain === "update") {
hostStyle.borderRadius = "0"; this.style.borderRadius = "0";
} else if (domain === "media_player") {
this.style.borderRadius = "8%";
} }
} else if (this.color) { } else if (this.color) {
// Externally provided overriding color wins over state color // Externally provided overriding color wins over state color
@ -179,12 +179,12 @@ export class StateBadge extends LitElement {
if (this.hass) { if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl); imageUrl = this.hass.hassUrl(imageUrl);
} }
hostStyle.backgroundImage = `url(${imageUrl})`; backgroundImage = `url(${imageUrl})`;
this._showIcon = false; this._showIcon = false;
} }
this._iconStyle = iconStyle; this._iconStyle = iconStyle;
Object.assign(this.style, hostStyle); this.style.backgroundImage = backgroundImage;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -95,25 +95,25 @@ class StateInfo extends LitElement {
:host { :host {
min-width: 120px; min-width: 120px;
white-space: nowrap; white-space: nowrap;
display: flex;
align-items: center;
} }
state-badge { state-badge {
float: left; flex: none;
}
:host([rtl]) state-badge {
float: right;
} }
.info { .info {
margin-left: 56px; margin-left: 8px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
min-width: 0;
} }
:host([rtl]) .info { :host([rtl]) .info {
margin-right: 56px; margin-right: 8px;
margin-left: 0; margin-left: 0;
text-align: right; text-align: right;
} }

View File

@ -4,7 +4,6 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { haStyle } from "../resources/styles"; import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-area-picker";
import "./ha-textfield"; import "./ha-textfield";
import type { HaTextField } from "./ha-textfield"; import type { HaTextField } from "./ha-textfield";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";

View File

@ -1,11 +1,9 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until"; import { until } from "lit/directives/until";
import { HomeAssistant } from "../types";
import { formatNumber } from "../common/number/format_number"; import { formatNumber } from "../common/number/format_number";
import { HomeAssistant } from "../types";
let jsYamlPromise: Promise<typeof import("../resources/js-yaml-dump")>;
@customElement("ha-attribute-value") @customElement("ha-attribute-value")
class HaAttributeValue extends LitElement { class HaAttributeValue extends LitElement {
@ -44,7 +42,7 @@ class HaAttributeValue extends LitElement {
${attributeValue} ${attributeValue}
</a> </a>
`; `;
} catch (_) { } catch {
// Nothing to do here // Nothing to do here
} }
} }
@ -55,15 +53,19 @@ class HaAttributeValue extends LitElement {
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) { const yaml = import("js-yaml").then(({ dump }) => dump(attributeValue));
jsYamlPromise = import("../resources/js-yaml-dump");
}
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.dump(attributeValue));
return html`<pre>${until(yaml, "")}</pre>`; return html`<pre>${until(yaml, "")}</pre>`;
} }
return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute); return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute);
} }
static styles = css`
pre {
margin: 0;
white-space: pre-wrap;
}
`;
} }
declare global { declare global {

View File

@ -26,6 +26,8 @@ export class HaButtonMenu extends LitElement {
@property({ type: Boolean }) public fixed = false; @property({ type: Boolean }) public fixed = false;
@property({ type: Boolean, attribute: "no-anchor" }) public noAnchor = false;
@query("mwc-menu", true) private _menu?: Menu; @query("mwc-menu", true) private _menu?: Menu;
public get items() { public get items() {
@ -82,7 +84,7 @@ export class HaButtonMenu extends LitElement {
if (this.disabled) { if (this.disabled) {
return; return;
} }
this._menu!.anchor = this; this._menu!.anchor = this.noAnchor ? null : this;
this._menu!.show(); this._menu!.show();
} }

View File

@ -17,6 +17,9 @@ export class HaButton extends Button {
.mdc-button { .mdc-button {
height: var(--button-height, 36px); height: var(--button-height, 36px);
} }
.trailing-icon {
display: flex;
}
`, `,
]; ];
} }

View File

@ -12,7 +12,6 @@ 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 { CodeMirror, loadCodeMirror } from "../resources/codemirror.ondemand";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-icon"; import "./ha-icon";
@ -58,7 +57,7 @@ export class HaCodeEditor extends ReactiveElement {
@state() private _value = ""; @state() private _value = "";
private _loadedCodeMirror?: CodeMirror; private _loadedCodeMirror?: typeof import("../resources/codemirror");
private _iconList?: Completion[]; private _iconList?: Completion[];
@ -110,7 +109,7 @@ export class HaCodeEditor extends ReactiveElement {
// Ensure CodeMirror module is loaded before any update // Ensure CodeMirror module is loaded before any update
protected override async scheduleUpdate() { protected override async scheduleUpdate() {
this._loadedCodeMirror ??= await loadCodeMirror(); this._loadedCodeMirror ??= await import("../resources/codemirror");
super.scheduleUpdate(); super.scheduleUpdate();
} }

View File

@ -64,6 +64,7 @@ class HaConfigEntryPicker extends LitElement {
type: "icon", type: "icon",
darkOptimized: this.hass.themes?.darkMode, darkOptimized: this.hass.themes?.darkMode,
})} })}
crossorigin="anonymous"
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
@error=${this._onImageError} @error=${this._onImageError}
@load=${this._onImageLoad} @load=${this._onImageLoad}

View File

@ -269,37 +269,61 @@ export class HaCountryPicker extends LitElement {
@property() public label?: string; @property() public label?: string;
@property() public countries?: string[];
@property() public helper?: string;
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@property({ type: Boolean, reflect: true }) public disabled = false; @property({ type: Boolean, reflect: true }) public disabled = false;
private _getOptions = memoizeOne((language?: string) => { @property({ type: Boolean }) public noSort = false;
const countryDisplayNames =
Intl && "DisplayNames" in Intl
? new Intl.DisplayNames(language, {
type: "region",
fallback: "code",
})
: undefined;
const options = COUNTRIES.map((country) => ({ private _getOptions = memoizeOne(
value: country, (language?: string, countries?: string[]) => {
label: countryDisplayNames ? countryDisplayNames.of(country)! : country, let options: { label: string; value: string }[] = [];
})); const countryDisplayNames =
options.sort((a, b) => Intl && "DisplayNames" in Intl
caseInsensitiveStringCompare(a.label, b.label, language) ? new Intl.DisplayNames(language, {
); type: "region",
return options; fallback: "code",
}); })
: undefined;
if (countries) {
options = countries.map((country) => ({
value: country,
label: countryDisplayNames
? countryDisplayNames.of(country)!
: country,
}));
} else {
options = COUNTRIES.map((country) => ({
value: country,
label: countryDisplayNames
? countryDisplayNames.of(country)!
: country,
}));
}
if (!this.noSort) {
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, language)
);
}
return options;
}
);
protected render() { protected render() {
const options = this._getOptions(this.language); const options = this._getOptions(this.language, this.countries);
return html` return html`
<ha-select <ha-select
.label=${this.label} .label=${this.label}
.value=${this.value} .value=${this.value}
.required=${this.required} .required=${this.required}
.helper=${this.helper}
.disabled=${this.disabled} .disabled=${this.disabled}
@selected=${this._changed} @selected=${this._changed}
@closed=${stopPropagation} @closed=${stopPropagation}

View File

@ -15,10 +15,11 @@ import {
CSSResultGroup, CSSResultGroup,
html, html,
LitElement, LitElement,
nothing,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { calcDate } from "../common/datetime/calc_date"; import { calcDate } from "../common/datetime/calc_date";
import { firstWeekdayIndex } from "../common/datetime/first_weekday"; import { firstWeekdayIndex } from "../common/datetime/first_weekday";
import { formatDate } from "../common/datetime/format_date"; import { formatDate } from "../common/datetime/format_date";
@ -29,6 +30,7 @@ import { HomeAssistant } from "../types";
import "./date-range-picker"; import "./date-range-picker";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./ha-textfield"; import "./ha-textfield";
import "./ha-icon-button";
export interface DateRangePickerRanges { export interface DateRangePickerRanges {
[key: string]: [Date, Date]; [key: string]: [Date, Date];
@ -54,8 +56,22 @@ export class HaDateRangePicker extends LitElement {
@property({ type: String }) private _rtlDirection = "ltr"; @property({ type: String }) private _rtlDirection = "ltr";
protected willUpdate() { @property({ type: Boolean }) private minimal = false;
if (!this.hasUpdated && this.ranges === undefined) {
@property() public openingDirection?: "right" | "left" | "center" | "inline";
@state() private _calcedOpeningDirection?:
| "right"
| "left"
| "center"
| "inline";
protected willUpdate(changedProps: PropertyValues) {
if (
(!this.hasUpdated && this.ranges === undefined) ||
(changedProps.has("hass") &&
this.hass?.localize !== changedProps.get("hass")?.localize)
) {
const today = new Date(); const today = new Date();
const weekStartsOn = firstWeekdayIndex(this.hass.locale); const weekStartsOn = firstWeekdayIndex(this.hass.locale);
const weekStart = calcDate( const weekStart = calcDate(
@ -133,41 +149,62 @@ export class HaDateRangePicker extends LitElement {
<date-range-picker <date-range-picker
?disabled=${this.disabled} ?disabled=${this.disabled}
?auto-apply=${this.autoApply} ?auto-apply=${this.autoApply}
?time-picker=${this.timePicker} time-picker=${this.timePicker}
twentyfour-hours=${this._hour24format} twentyfour-hours=${this._hour24format}
start-date=${this.startDate} start-date=${this.startDate}
end-date=${this.endDate} end-date=${this.endDate}
?ranges=${this.ranges !== false} ?ranges=${this.ranges !== false}
opening-direction=${this.openingDirection ||
this._calcedOpeningDirection}
first-day=${firstWeekdayIndex(this.hass.locale)} first-day=${firstWeekdayIndex(this.hass.locale)}
> >
<div slot="input" class="date-range-inputs"> <div slot="input" class="date-range-inputs" @click=${this._handleClick}>
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon> ${!this.minimal
<ha-textfield ? html`<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
.value=${this.timePicker <ha-textfield
? formatDateTime( .value=${this.timePicker
this.startDate, ? formatDateTime(
this.hass.locale, this.startDate,
this.hass.config this.hass.locale,
) this.hass.config
: formatDate(this.startDate, this.hass.locale, this.hass.config)} )
.label=${this.hass.localize( : formatDate(
"ui.components.date-range-picker.start_date" this.startDate,
)} this.hass.locale,
.disabled=${this.disabled} this.hass.config
@click=${this._handleInputClick} )}
readonly .label=${this.hass.localize(
></ha-textfield> "ui.components.date-range-picker.start_date"
<ha-textfield )}
.value=${this.timePicker .disabled=${this.disabled}
? formatDateTime(this.endDate, this.hass.locale, this.hass.config) @click=${this._handleInputClick}
: formatDate(this.endDate, this.hass.locale, this.hass.config)} readonly
.label=${this.hass.localize( ></ha-textfield>
"ui.components.date-range-picker.end_date" <ha-textfield
)} .value=${this.timePicker
.disabled=${this.disabled} ? formatDateTime(
@click=${this._handleInputClick} this.endDate,
readonly this.hass.locale,
></ha-textfield> this.hass.config
)
: formatDate(
this.endDate,
this.hass.locale,
this.hass.config
)}
.label=${this.hass.localize(
"ui.components.date-range-picker.end_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></ha-textfield>`
: html`<ha-icon-button
.label=${this.hass.localize(
"ui.components.date-range-picker.select_date_range"
)}
.path=${mdiCalendar}
></ha-icon-button>`}
</div> </div>
${this.ranges ${this.ranges
? html`<div ? html`<div
@ -181,7 +218,7 @@ export class HaDateRangePicker extends LitElement {
)} )}
</mwc-list> </mwc-list>
</div>` </div>`
: ""} : nothing}
<div slot="footer" class="date-range-footer"> <div slot="footer" class="date-range-footer">
<mwc-button @click=${this._cancelDateRange} <mwc-button @click=${this._cancelDateRange}
>${this.hass.localize("ui.common.cancel")}</mwc-button >${this.hass.localize("ui.common.cancel")}</mwc-button
@ -225,6 +262,22 @@ export class HaDateRangePicker extends LitElement {
} }
} }
private _handleClick() {
// calculate opening direction if not set
if (!this._dateRangePicker.open && !this.openingDirection) {
const datePickerPosition = this.getBoundingClientRect().x;
let opens: "right" | "left" | "center" | "inline";
if (datePickerPosition > (2 * window.innerWidth) / 3) {
opens = "left";
} else if (datePickerPosition < window.innerWidth / 3) {
opens = "right";
} else {
opens = "center";
}
this._calcedOpeningDirection = opens;
}
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
ha-svg-icon { ha-svg-icon {
@ -234,6 +287,10 @@ export class HaDateRangePicker extends LitElement {
direction: var(--direction); direction: var(--direction);
} }
ha-icon-button {
direction: var(--direction);
}
.date-range-inputs { .date-range-inputs {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -163,10 +163,7 @@ export class HaExpansionPanel extends LitElement {
box-shadow: none; box-shadow: none;
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
border-color: var( border-color: var(--outline-color);
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, 12px);
} }

View File

@ -66,6 +66,10 @@ export const computeInitialHaFormData = (
typeof firstOption === "string" ? firstOption : firstOption.value; typeof firstOption === "string" ? firstOption : firstOption.value;
data[field.name] = selector.select.multiple ? [val] : val; data[field.name] = selector.select.multiple ? [val] : val;
} }
} else if ("country" in selector) {
if (selector.country?.countries?.length) {
data[field.name] = selector.country.countries[0];
}
} else if ("duration" in selector) { } else if ("duration" in selector) {
data[field.name] = { data[field.name] = {
hours: 0, hours: 0,

View File

@ -57,8 +57,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
` `
: ""} : ""}
<ha-slider <ha-slider
pin labeled
ignore-bar-touch
.value=${this._value} .value=${this._value}
.min=${this.schema.valueMin} .min=${this.schema.valueMin}
.max=${this.schema.valueMax} .max=${this.schema.valueMax}

View File

@ -138,15 +138,13 @@ export class HaFormString extends LitElement implements HaFormElement {
} }
ha-icon-button { ha-icon-button {
position: absolute; position: absolute;
top: 1em; top: 8px;
right: 12px; right: 8px;
--mdc-icon-button-size: 24px;
color: var(--secondary-text-color);
}
ha-icon-button {
inset-inline-start: initial; inset-inline-start: initial;
inset-inline-end: 12px; inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction); direction: var(--direction);
} }
`; `;

View File

@ -1,9 +1,9 @@
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
PropertyValues, PropertyValues,
css,
html,
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -11,12 +11,12 @@ import { fireEvent } from "../common/dom/fire_event";
import { debounce } from "../common/util/debounce"; import { debounce } from "../common/util/debounce";
import { CustomIcon, customIcons } from "../data/custom_icons"; import { CustomIcon, customIcons } from "../data/custom_icons";
import { import {
checkCacheVersion,
Chunks, Chunks,
findIconChunk,
getIcon,
Icons, Icons,
MDI_PREFIXES, MDI_PREFIXES,
checkCacheVersion,
findIconChunk,
getIcon,
writeCache, writeCache,
} from "../data/iconsets"; } from "../data/iconsets";
import "./ha-svg-icon"; import "./ha-svg-icon";
@ -47,6 +47,8 @@ export class HaIcon extends LitElement {
@state() private _path?: string; @state() private _path?: string;
@state() private _secondaryPath?: string;
@state() private _viewBox?: string; @state() private _viewBox?: string;
@state() private _legacy = false; @state() private _legacy = false;
@ -55,6 +57,7 @@ export class HaIcon extends LitElement {
super.willUpdate(changedProps); super.willUpdate(changedProps);
if (changedProps.has("icon")) { if (changedProps.has("icon")) {
this._path = undefined; this._path = undefined;
this._secondaryPath = undefined;
this._viewBox = undefined; this._viewBox = undefined;
this._loadIcon(); this._loadIcon();
} }
@ -70,6 +73,7 @@ export class HaIcon extends LitElement {
} }
return html`<ha-svg-icon return html`<ha-svg-icon
.path=${this._path} .path=${this._path}
.secondaryPath=${this._secondaryPath}
.viewBox=${this._viewBox} .viewBox=${this._viewBox}
></ha-svg-icon>`; ></ha-svg-icon>`;
} }
@ -175,6 +179,7 @@ export class HaIcon extends LitElement {
return; return;
} }
this._path = icon.path; this._path = icon.path;
this._secondaryPath = icon.secondaryPath;
this._viewBox = icon.viewBox; this._viewBox = icon.viewBox;
} }

View File

@ -1,90 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-icon";
import "./ha-slider";
class HaLabeledSlider extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
}
.title {
margin: 5px 0 8px;
color: var(--primary-text-color);
}
.slider-container {
display: flex;
}
ha-icon {
margin-top: 4px;
color: var(--secondary-text-color);
}
ha-slider {
flex-grow: 1;
background-image: var(--ha-slider-background);
border-radius: 4px;
}
</style>
<div class="title">[[_getTitle()]]</div>
<div class="extra-container"><slot name="extra"></slot></div>
<div class="slider-container">
<ha-icon icon="[[icon]]" hidden$="[[!icon]]"></ha-icon>
<ha-slider
min="[[min]]"
max="[[max]]"
step="[[step]]"
pin="[[pin]]"
disabled="[[disabled]]"
value="{{value}}"
></ha-slider>
</div>
<template is="dom-if" if="[[helper]]">
<ha-input-helper-text>[[helper]]</ha-input-helper-text>
</template>
`;
}
_getTitle() {
return `${this.caption}${this.caption && this.required ? " *" : ""}`;
}
static get properties() {
return {
caption: String,
disabled: Boolean,
required: Boolean,
min: Number,
max: Number,
pin: Boolean,
step: Number,
helper: String,
extra: {
type: Boolean,
value: false,
},
ignoreBarTouch: {
type: Boolean,
value: true,
},
icon: {
type: String,
value: "",
},
value: {
type: Number,
notify: true,
},
};
}
}
customElements.define("ha-labeled-slider", HaLabeledSlider);

View File

@ -0,0 +1,96 @@
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-icon";
import "./ha-slider";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-labeled-slider")
class HaLabeledSlider extends LitElement {
@property() public labeled? = false;
@property() public caption?: string;
@property() public disabled?: boolean;
@property() public required?: boolean;
@property() public min: number = 0;
@property() public max: number = 100;
@property() public step: number = 1;
@property() public helper?: string;
@property() public extra = false;
@property() public icon?: string;
@property() public value?: number;
protected render() {
return html`
<div class="title">${this._getTitle()}</div>
<div class="extra-container"><slot name="extra"></slot></div>
<div class="slider-container">
${this.icon ? html`<ha-icon icon=${this.icon}></ha-icon>` : nothing}
<ha-slider
.min=${this.min}
.max=${this.max}
.step=${this.step}
labeled=${this.labeled}
.disabled=${this.disabled}
.value=${this.value}
@change=${this._inputChanged}
></ha-slider>
</div>
${this.helper
? html`<ha-input-helper-text> ${this.helper} </ha-input-helper-text>`
: nothing}
`;
}
private _getTitle(): string {
return `${this.caption}${this.caption && this.required ? " *" : ""}`;
}
private _inputChanged(ev) {
fireEvent(this, "value-changed", {
value: Number((ev.target as any).value),
});
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
.title {
margin: 5px 0 8px;
color: var(--primary-text-color);
}
.slider-container {
display: flex;
}
ha-icon {
margin-top: 8px;
color: var(--secondary-text-color);
}
ha-slider {
flex-grow: 1;
background-image: var(--ha-slider-background);
border-radius: 4px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-labeled-slider": HaLabeledSlider;
}
}

View File

@ -1,19 +1,16 @@
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { OutlinedButton } from "@material/web/button/internal/outlined-button"; import { MdOutlinedButton } from "@material/web/button/outlined-button";
import { styles as outlinedStyles } from "@material/web/button/internal/outlined-styles.css";
import { styles as sharedStyles } from "@material/web/button/internal/shared-styles.css";
@customElement("ha-outlined-button") @customElement("ha-outlined-button")
export class HaOutlinedButton extends OutlinedButton { export class HaOutlinedButton extends MdOutlinedButton {
static override styles = [ static override styles = [
sharedStyles, ...super.styles,
outlinedStyles,
css` css`
:host { :host {
--ha-icon-display: block; --ha-icon-display: block;
--md-sys-color-primary: var(--primary-text-color); --md-sys-color-primary: var(--primary-text-color);
--md-sys-color-outline: var(--divider-color); --md-sys-color-outline: var(--outline-color);
} }
`, `,
]; ];

View File

@ -1,21 +1,11 @@
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 { MdOutlinedIconButton } from "@material/web/iconbutton/outlined-icon-button";
import { styles as outlinedStyles } from "@material/web/iconbutton/internal/outlined-styles.css";
import { styles as sharedStyles } from "@material/web/iconbutton/internal/shared-styles.css";
@customElement("ha-outlined-icon-button") @customElement("ha-outlined-icon-button")
export class HaOutlinedIconButton extends IconButton { export class HaOutlinedIconButton extends MdOutlinedIconButton {
protected override getRenderClasses() {
return {
...super.getRenderClasses(),
outlined: true,
};
}
static override styles = [ static override styles = [
sharedStyles, ...super.styles,
outlinedStyles,
css` css`
:host { :host {
--ha-icon-display: block; --ha-icon-display: block;

View File

@ -154,6 +154,8 @@ export class HaRelatedItems extends LitElement {
useFallback: true, useFallback: true,
darkOptimized: this.hass.themes?.darkMode, darkOptimized: this.hass.themes?.darkMode,
})} })}
crossorigin="anonymous"
referrerpolicy="no-referrer"
alt=${entry.domain} alt=${entry.domain}
slot="graphic" slot="graphic"
/> />

View File

@ -1,15 +1,32 @@
import { SelectBase } from "@material/mwc-select/mwc-select-base"; import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { styles } from "@material/mwc-select/mwc-select.css"; import { styles } from "@material/mwc-select/mwc-select.css";
import { mdiClose } from "@mdi/js";
import { css, html, nothing } from "lit"; import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { debounce } from "../common/util/debounce"; import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status"; import { nextRender } from "../common/util/render-status";
import "./ha-icon-button";
@customElement("ha-select") @customElement("ha-select")
export class HaSelect extends SelectBase { export class HaSelect extends SelectBase {
// @ts-ignore // @ts-ignore
@property({ type: Boolean }) public icon?: boolean; @property({ type: Boolean }) public icon?: boolean;
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
protected override render() {
return html`
${super.render()}
${this.clearable && !this.required && !this.disabled && this.value
? html`<ha-icon-button
label="clear"
@click=${this._clearValue}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
`;
}
protected override renderLeadingIcon() { protected override renderLeadingIcon() {
if (!this.icon) { if (!this.icon) {
return nothing; return nothing;
@ -33,6 +50,15 @@ export class HaSelect extends SelectBase {
); );
} }
private _clearValue(): void {
if (this.disabled || !this.value) {
return;
}
this.valueSetDirectly = true;
this.select(-1);
this.mdcFoundation.handleChange();
}
private _translationsUpdated = debounce(async () => { private _translationsUpdated = debounce(async () => {
await nextRender(); await nextRender();
this.layoutOptions(); this.layoutOptions();
@ -41,6 +67,9 @@ export class HaSelect extends SelectBase {
static override styles = [ static override styles = [
styles, styles,
css` css`
:host([clearable]) {
position: relative;
}
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon { .mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
@ -68,6 +97,23 @@ export class HaSelect extends SelectBase {
.mdc-select__anchor .mdc-floating-label--float-above { .mdc-select__anchor .mdc-floating-label--float-above {
transform-origin: var(--float-start); transform-origin: var(--float-start);
} }
.mdc-select__selected-text-container {
padding-inline-end: var(--select-selected-text-padding-end, 0px);
}
:host([clearable]) .mdc-select__selected-text-container {
padding-inline-end: var(--select-selected-text-padding-end, 12px);
}
ha-icon-button {
position: absolute;
top: 10px;
right: 28px;
--mdc-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
inset-inline-start: initial;
inset-inline-end: 28px;
direction: var(--direction);
}
`, `,
]; ];
} }

View File

@ -24,7 +24,7 @@ export class HaColorTempSelector extends LitElement {
protected render() { protected render() {
return html` return html`
<ha-labeled-slider <ha-labeled-slider
pin labeled
icon="hass:thermometer" icon="hass:thermometer"
.caption=${this.label || ""} .caption=${this.label || ""}
.min=${this.selector.color_temp?.min_mireds ?? 153} .min=${this.selector.color_temp?.min_mireds ?? 153}
@ -33,27 +33,25 @@ export class HaColorTempSelector extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.helper=${this.helper} .helper=${this.helper}
.required=${this.required} .required=${this.required}
@change=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-labeled-slider> ></ha-labeled-slider>
`; `;
} }
private _valueChanged(ev: CustomEvent) { private _valueChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: Number((ev.target as any).value), value: Number((ev.detail as any).value),
}); });
} }
static styles = css` static styles = css`
ha-labeled-slider { ha-labeled-slider {
--ha-slider-background: -webkit-linear-gradient( --ha-slider-background: linear-gradient(
var(--float-end), to var(--float-end),
rgb(255, 160, 0) 0%, rgb(255, 160, 0) 0%,
white 50%, white 50%,
rgb(166, 209, 255) 100% rgb(166, 209, 255) 100%
); );
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
--paper-slider-knob-start-border-color: var(--primary-color);
} }
`; `;
} }

View File

@ -0,0 +1,49 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { CountrySelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-country-picker";
@customElement("ha-selector-country")
export class HaCountrySelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: CountrySelector;
@property() public value?: any;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-country-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.countries=${this.selector.country?.countries}
.noSort=${this.selector.country?.no_sort}
.disabled=${this.disabled}
.required=${this.required}
></ha-country-picker>
`;
}
static styles = css`
ha-country-picker {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-country": HaCountrySelector;
}
}

View File

@ -26,7 +26,7 @@ export class HaDateSelector extends LitElement {
.label=${this.label} .label=${this.label}
.locale=${this.hass.locale} .locale=${this.hass.locale}
.disabled=${this.disabled} .disabled=${this.disabled}
.value=${this.value} .value=${typeof this.value === "string" ? this.value : undefined}
.required=${this.required} .required=${this.required}
.helper=${this.helper} .helper=${this.helper}
> >

View File

@ -30,7 +30,8 @@ export class HaDateTimeSelector extends LitElement {
@query("ha-time-input") private _timeInput!: HaTimeInput; @query("ha-time-input") private _timeInput!: HaTimeInput;
protected render() { protected render() {
const values = this.value?.split(" "); const values =
typeof this.value === "string" ? this.value.split(" ") : undefined;
return html` return html`
<div class="input"> <div class="input">

View File

@ -1,4 +1,4 @@
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } 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";
@ -26,8 +26,22 @@ export class HaNumberSelector extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
private _valueStr = "";
protected willUpdate(changedProps: PropertyValues) {
if (changedProps.has("value")) {
if (this.value !== Number(this._valueStr)) {
this._valueStr =
!this.value || isNaN(this.value) ? "" : this.value.toString();
}
}
}
protected render() { protected render() {
const isBox = this.selector.number?.mode === "box"; const isBox =
this.selector.number?.mode === "box" ||
this.selector.number?.min === undefined ||
this.selector.number?.max === undefined;
return html` return html`
<div class="input"> <div class="input">
@ -37,6 +51,7 @@ export class HaNumberSelector extends LitElement {
? html`${this.label}${this.required ? "*" : ""}` ? html`${this.label}${this.required ? "*" : ""}`
: ""} : ""}
<ha-slider <ha-slider
labeled
.min=${this.selector.number?.min} .min=${this.selector.number?.min}
.max=${this.selector.number?.max} .max=${this.selector.number?.max}
.value=${this.value ?? ""} .value=${this.value ?? ""}
@ -45,8 +60,6 @@ export class HaNumberSelector extends LitElement {
: this.selector.number?.step ?? 1} : this.selector.number?.step ?? 1}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
pin
ignore-bar-touch
@change=${this._handleSliderChange} @change=${this._handleSliderChange}
> >
</ha-slider> </ha-slider>
@ -57,14 +70,12 @@ export class HaNumberSelector extends LitElement {
(this.selector.number?.step ?? 1) % 1 !== 0 (this.selector.number?.step ?? 1) % 1 !== 0
? "decimal" ? "decimal"
: "numeric"} : "numeric"}
.label=${this.selector.number?.mode !== "box" .label=${!isBox ? undefined : this.label}
? undefined
: this.label}
.placeholder=${this.placeholder} .placeholder=${this.placeholder}
class=${classMap({ single: this.selector.number?.mode === "box" })} class=${classMap({ single: isBox })}
.min=${this.selector.number?.min} .min=${this.selector.number?.min}
.max=${this.selector.number?.max} .max=${this.selector.number?.max}
.value=${this.value ?? ""} .value=${this._valueStr ?? ""}
.step=${this.selector.number?.step ?? 1} .step=${this.selector.number?.step ?? 1}
helperPersistent helperPersistent
.helper=${isBox ? this.helper : undefined} .helper=${isBox ? this.helper : undefined}
@ -73,7 +84,7 @@ export class HaNumberSelector extends LitElement {
.suffix=${this.selector.number?.unit_of_measurement} .suffix=${this.selector.number?.unit_of_measurement}
type="number" type="number"
autoValidate autoValidate
?no-spinner=${this.selector.number?.mode !== "box"} ?no-spinner=${!isBox}
@input=${this._handleInputChange} @input=${this._handleInputChange}
> >
</ha-textfield> </ha-textfield>
@ -86,6 +97,7 @@ export class HaNumberSelector extends LitElement {
private _handleInputChange(ev) { private _handleInputChange(ev) {
ev.stopPropagation(); ev.stopPropagation();
this._valueStr = ev.target.value;
const value = const value =
ev.target.value === "" || isNaN(ev.target.value) ev.target.value === "" || isNaN(ev.target.value)
? undefined ? undefined

View File

@ -1,11 +1,16 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiClose } from "@mdi/js"; import { mdiClose, mdiDrag } from "@mdi/js";
import { css, html, LitElement } from "lit"; import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { SortableEvent } from "sortablejs";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation"; import { stopPropagation } from "../../common/dom/stop_propagation";
import { ensureArray } from "../../common/array/ensure-array"; import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { SelectOption, SelectSelector } from "../../data/selector"; import type { SelectOption, SelectSelector } from "../../data/selector";
import { sortableStyles } from "../../resources/ha-sortable-style";
import { SortableInstance } from "../../resources/sortable";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-checkbox"; import "../ha-checkbox";
import "../ha-chip"; import "../ha-chip";
@ -13,10 +18,9 @@ import "../ha-chip-set";
import "../ha-combo-box"; import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box";
import "../ha-formfield"; import "../ha-formfield";
import "../ha-input-helper-text";
import "../ha-radio"; import "../ha-radio";
import "../ha-select"; import "../ha-select";
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 {
@ -38,6 +42,68 @@ export class HaSelectSelector extends LitElement {
@query("ha-combo-box", true) private comboBox!: HaComboBox; @query("ha-combo-box", true) private comboBox!: HaComboBox;
private _sortable?: SortableInstance;
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("value") || changedProps.has("selector")) {
const sortableNeeded =
this.selector.select?.multiple &&
this.selector.select.reorder &&
this.value?.length;
if (!this._sortable && sortableNeeded) {
this._createSortable();
} else if (this._sortable && !sortableNeeded) {
this._destroySortable();
}
}
}
private async _createSortable() {
const Sortable = (await import("../../resources/sortable")).default;
this._sortable = new Sortable(
this.shadowRoot!.querySelector("ha-chip-set")!,
{
animation: 150,
fallbackClass: "sortable-fallback",
draggable: "ha-chip",
onChoose: (evt: SortableEvent) => {
(evt.item as any).placeholder =
document.createComment("sort-placeholder");
evt.item.after((evt.item as any).placeholder);
},
onEnd: (evt: SortableEvent) => {
// put back in original location
if ((evt.item as any).placeholder) {
(evt.item as any).placeholder.replaceWith(evt.item);
delete (evt.item as any).placeholder;
}
this._dragged(evt);
},
}
);
}
private _dragged(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) return;
this._move(ev.oldIndex!, ev.newIndex!);
}
private _move(index: number, newIndex: number) {
const value = this.value as string[];
const newValue = value.concat();
const element = newValue.splice(index, 1)[0];
newValue.splice(newIndex, 0, element);
this.value = newValue;
fireEvent(this, "value-changed", {
value: newValue,
});
}
private _destroySortable() {
this._sortable?.destroy();
this._sortable = undefined;
}
private _filter = ""; private _filter = "";
protected render() { protected render() {
@ -71,7 +137,11 @@ export class HaSelectSelector extends LitElement {
); );
} }
if (!this.selector.select?.custom_value && this._mode === "list") { if (
!this.selector.select?.custom_value &&
!this.selector.select?.reorder &&
this._mode === "list"
) {
if (!this.selector.select?.multiple) { if (!this.selector.select?.multiple) {
return html` return html`
<div> <div>
@ -124,23 +194,39 @@ export class HaSelectSelector extends LitElement {
return html` return html`
${value?.length ${value?.length
? html`<ha-chip-set> ? html`
${value.map( <ha-chip-set>
(item, idx) => html` ${repeat(
<ha-chip hasTrailingIcon> value,
${options.find((option) => option.value === item)?.label || (item) => item,
item} (item, idx) => html`
<ha-svg-icon <ha-chip
slot="trailing-icon" hasTrailingIcon
.path=${mdiClose} .hasIcon=${this.selector.select?.reorder}
.idx=${idx} >
@click=${this._removeItem} ${this.selector.select?.reorder
></ha-svg-icon> ? html`
</ha-chip> <ha-svg-icon
` slot="icon"
)} .path=${mdiDrag}
</ha-chip-set>` data-handle
: ""} ></ha-svg-icon>
`
: nothing}
${options.find((option) => option.value === item)
?.label || item}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiClose}
.idx=${idx}
@click=${this._removeItem}
></ha-svg-icon>
</ha-chip>
`
)}
</ha-chip-set>
`
: nothing}
<ha-combo-box <ha-combo-box
item-value-path="value" item-value-path="value"
@ -198,6 +284,7 @@ export class HaSelectSelector extends LitElement {
.helper=${this.helper ?? ""} .helper=${this.helper ?? ""}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
clearable
@closed=${stopPropagation} @closed=${stopPropagation}
@selected=${this._valueChanged} @selected=${this._valueChanged}
> >
@ -228,7 +315,7 @@ export class HaSelectSelector extends LitElement {
private _valueChanged(ev) { private _valueChanged(ev) {
ev.stopPropagation(); ev.stopPropagation();
const value = ev.detail?.value || ev.target.value; const value = ev.detail?.value || ev.target.value;
if (this.disabled || value === undefined || value === this.value) { if (this.disabled || value === undefined || value === (this.value ?? "")) {
return; return;
} }
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
@ -330,16 +417,22 @@ export class HaSelectSelector extends LitElement {
this.comboBox.filteredItems = filteredItems; this.comboBox.filteredItems = filteredItems;
} }
static styles = css` static styles = [
ha-select, sortableStyles,
mwc-formfield, css`
ha-formfield { :host {
display: block; position: relative;
} }
mwc-list-item[disabled] { ha-select,
--mdc-theme-text-primary-on-background: var(--disabled-text-color); mwc-formfield,
} ha-formfield {
`; display: block;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
`,
];
} }
declare global { declare global {

View File

@ -111,13 +111,13 @@ export class HaTextSelector extends LitElement {
} }
ha-icon-button { ha-icon-button {
position: absolute; position: absolute;
top: 10px; top: 8px;
right: 10px; right: 8px;
--mdc-icon-button-size: 36px; inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
inset-inline-start: initial;
inset-inline-end: 10px;
direction: var(--direction); direction: var(--direction);
} }
`; `;

View File

@ -23,7 +23,7 @@ export class HaTimeSelector extends LitElement {
protected render() { protected render() {
return html` return html`
<ha-time-input <ha-time-input
.value=${this.value} .value=${typeof this.value === "string" ? this.value : undefined}
.locale=${this.hass.locale} .locale=${this.hass.locale}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}

View File

@ -21,6 +21,7 @@ const LOAD_ELEMENTS = {
config_entry: () => import("./ha-selector-config-entry"), config_entry: () => import("./ha-selector-config-entry"),
conversation_agent: () => import("./ha-selector-conversation-agent"), conversation_agent: () => import("./ha-selector-conversation-agent"),
constant: () => import("./ha-selector-constant"), constant: () => import("./ha-selector-constant"),
country: () => import("./ha-selector-country"),
date: () => import("./ha-selector-date"), date: () => import("./ha-selector-date"),
datetime: () => import("./ha-selector-datetime"), datetime: () => import("./ha-selector-datetime"),
device: () => import("./ha-selector-device"), device: () => import("./ha-selector-device"),

View File

@ -2,9 +2,9 @@ import "@material/mwc-button/mwc-button";
import { import {
mdiBell, mdiBell,
mdiCalendar, mdiCalendar,
mdiCart,
mdiCellphoneCog, mdiCellphoneCog,
mdiChartBox, mdiChartBox,
mdiClipboardList,
mdiClose, mdiClose,
mdiCog, mdiCog,
mdiFormatListBulletedType, mdiFormatListBulletedType,
@ -50,7 +50,7 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
import { UpdateEntity, updateCanInstall } from "../data/update"; import { UpdateEntity, updateCanInstall } from "../data/update";
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { SortableInstance, loadSortable } from "../resources/sortable.ondemand"; import type { SortableInstance } from "../resources/sortable";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types"; import type { HomeAssistant, PanelInfo, Route } from "../types";
import "./ha-icon"; import "./ha-icon";
@ -81,7 +81,7 @@ const PANEL_ICONS = {
lovelace: mdiViewDashboard, lovelace: mdiViewDashboard,
map: mdiTooltipAccount, map: mdiTooltipAccount,
"media-browser": mdiPlayBoxMultiple, "media-browser": mdiPlayBoxMultiple,
"shopping-list": mdiCart, todo: mdiClipboardList,
}; };
const panelSorter = ( const panelSorter = (
@ -689,7 +689,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
} }
private async _createSortable() { private async _createSortable() {
const Sortable = await loadSortable(); const Sortable = (await import("../resources/sortable")).default;
this._sortable = new Sortable( this._sortable = new Sortable(
this.shadowRoot!.getElementById("sortable")!, this.shadowRoot!.getElementById("sortable")!,
{ {

View File

@ -1,117 +0,0 @@
import "@polymer/paper-slider";
const PaperSliderClass = customElements.get("paper-slider");
let subTemplate;
export class HaSlider extends PaperSliderClass {
static get template() {
if (!subTemplate) {
subTemplate = PaperSliderClass.template.cloneNode(true);
const superStyle = subTemplate.content.querySelector("style");
// append style to add mirroring of pin in RTL
superStyle.appendChild(
document.createTextNode(`
:host([dir="rtl"]) #sliderContainer.pin.expand > .slider-knob > .slider-knob-inner::after {
-webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important;
transform: scale(1) translate(0, -17px) scaleX(-1) !important;
}
.pin > .slider-knob > .slider-knob-inner {
font-size: var(--ha-slider-pin-font-size, 15px);
line-height: normal;
cursor: pointer;
}
.disabled.ring > .slider-knob > .slider-knob-inner {
background-color: var(--paper-slider-disabled-knob-color, var(--disabled-text-color));
border: 2px solid var(--paper-slider-disabled-knob-color, var(--disabled-text-color));
}
.pin > .slider-knob > .slider-knob-inner::before {
top: unset;
margin-left: unset;
bottom: calc(15px + var(--calculated-paper-slider-height)/2);
left: 50%;
width: 2.6em;
height: 2.6em;
-webkit-transform-origin: left bottom;
transform-origin: left bottom;
-webkit-transform: rotate(-45deg) scale(0) translate(0);
transform: rotate(-45deg) scale(0) translate(0);
}
.pin.expand > .slider-knob > .slider-knob-inner::before {
-webkit-transform: rotate(-45deg) scale(1) translate(7px, -7px);
transform: rotate(-45deg) scale(1) translate(7px, -7px);
}
.pin > .slider-knob > .slider-knob-inner::after {
top: unset;
font-size: unset;
bottom: calc(15px + var(--calculated-paper-slider-height)/2);
left: 50%;
margin-left: -1.3em;
width: 2.6em;
height: 2.5em;
-webkit-transform-origin: center bottom;
transform-origin: center bottom;
-webkit-transform: scale(0) translate(0);
transform: scale(0) translate(0);
}
.pin.expand > .slider-knob > .slider-knob-inner::after {
-webkit-transform: scale(1) translate(0, -10px);
transform: scale(1) translate(0, -10px);
}
.slider-input {
width: 54px;
}
`)
);
}
return subTemplate;
}
_setImmediateValue(newImmediateValue) {
super._setImmediateValue(
this.step >= 1
? Math.round(newImmediateValue)
: Math.round(newImmediateValue * 100) / 100
);
}
_calcStep(value) {
if (!this.step) {
return parseFloat(value);
}
const numSteps = Math.round((value - this.min) / this.step);
const stepStr = this.step.toString();
const stepPointAt = stepStr.indexOf(".");
if (stepPointAt !== -1) {
/**
* For small values of this.step, if we calculate the step using
* For non-integer values of this.step, if we calculate the step using
* `Math.round(value / step) * step` we may hit a precision point issue
* eg. 0.1 * 0.2 = 0.020000000000000004
* http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
*
* as a work around we can round with the decimal precision of `step`
*/
const precision = 10 ** (stepStr.length - stepPointAt - 1);
return (
Math.round((numSteps * this.step + this.min) * precision) / precision
);
}
return numSteps * this.step + this.min;
}
}
customElements.define("ha-slider", HaSlider);

View File

@ -0,0 +1,26 @@
import { customElement } from "lit/decorators";
import { MdSlider } from "@material/web/slider/slider";
import { CSSResult, css } from "lit";
@customElement("ha-slider")
export class HaSlider extends MdSlider {
static override styles: CSSResult[] = [
...MdSlider.styles,
css`
:host {
--md-sys-color-primary: var(--primary-color);
--md-sys-color-outline: var(--outline-color);
min-width: 100px;
min-inline-size: 100px;
width: 200px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-slider": HaSlider;
}
}

View File

@ -1,10 +1,19 @@
import { css, CSSResultGroup, LitElement, svg, SVGTemplateResult } from "lit"; import {
css,
CSSResultGroup,
LitElement,
nothing,
svg,
SVGTemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@customElement("ha-svg-icon") @customElement("ha-svg-icon")
export class HaSvgIcon extends LitElement { export class HaSvgIcon extends LitElement {
@property() public path?: string; @property() public path?: string;
@property() public secondaryPath?: string;
@property() public viewBox?: string; @property() public viewBox?: string;
protected render(): SVGTemplateResult { protected render(): SVGTemplateResult {
@ -13,11 +22,20 @@ export class HaSvgIcon extends LitElement {
viewBox=${this.viewBox || "0 0 24 24"} viewBox=${this.viewBox || "0 0 24 24"}
preserveAspectRatio="xMidYMid meet" preserveAspectRatio="xMidYMid meet"
focusable="false" focusable="false"
role="img" role="img"
aria-hidden="true" aria-hidden="true"
> >
<g> <g>
${this.path ? svg`<path d=${this.path}></path>` : ""} ${
this.path
? svg`<path class="primary-path" d=${this.path}></path>`
: nothing
}
${
this.secondaryPath
? svg`<path class="secondary-path" d=${this.secondaryPath}></path>`
: nothing
}
</g> </g>
</svg>`; </svg>`;
} }
@ -30,7 +48,7 @@ export class HaSvgIcon extends LitElement {
justify-content: center; justify-content: center;
position: relative; position: relative;
vertical-align: middle; vertical-align: middle;
fill: currentcolor; fill: var(--icon-primary-color, currentcolor);
width: var(--mdc-icon-size, 24px); width: var(--mdc-icon-size, 24px);
height: var(--mdc-icon-size, 24px); height: var(--mdc-icon-size, 24px);
} }
@ -40,6 +58,13 @@ export class HaSvgIcon extends LitElement {
pointer-events: none; pointer-events: none;
display: block; display: block;
} }
path.primary-path {
opacity: var(--icon-primary-opactity, 1);
}
path.secondary-path {
fill: var(--icon-secondary-color, currentcolor);
opacity: var(--icon-secondary-opactity, 0.5);
}
`; `;
} }
} }

View File

@ -0,0 +1,320 @@
import {
addHasRemoveClass,
BaseElement,
} from "@material/mwc-base/base-element";
import { supportsPassiveEventListener } from "@material/mwc-base/utils";
import { MDCTopAppBarAdapter } from "@material/top-app-bar/adapter";
import { strings } from "@material/top-app-bar/constants";
import MDCFixedTopAppBarFoundation from "@material/top-app-bar/fixed/foundation";
import { html, css, nothing } from "lit";
import { property, query, customElement } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
import { haStyleScrollbar } from "../resources/styles";
export const passiveEventOptionsIfSupported = supportsPassiveEventListener
? { passive: true }
: undefined;
@customElement("ha-two-pane-top-app-bar-fixed")
export abstract class TopAppBarBaseBase extends BaseElement {
protected override mdcFoundation!: MDCFixedTopAppBarFoundation;
protected override mdcFoundationClass = MDCFixedTopAppBarFoundation;
@query(".mdc-top-app-bar") protected mdcRoot!: HTMLElement;
// _actionItemsSlot should have type HTMLSlotElement, but when TypeScript's
// emitDecoratorMetadata is enabled, the HTMLSlotElement constructor will
// be emitted into the runtime, which will cause an "HTMLSlotElement is
// undefined" error in browsers that don't define it (e.g. IE11).
@query('slot[name="actionItems"]') protected _actionItemsSlot!: HTMLElement;
protected _scrollTarget!: HTMLElement | Window;
@property({ type: Boolean }) centerTitle = false;
@property({ type: Boolean, reflect: true }) prominent = false;
@property({ type: Boolean, reflect: true }) dense = false;
@property({ type: Boolean }) pane = false;
@property({ type: Boolean }) footer = false;
@query(".content") private _contentElement!: HTMLElement;
@query(".pane .ha-scrollbar") private _paneElement?: HTMLElement;
@property({ type: Object })
get scrollTarget() {
return this._scrollTarget || window;
}
set scrollTarget(value) {
this.unregisterListeners();
const old = this.scrollTarget;
this._scrollTarget = value;
this.updateRootPosition();
this.requestUpdate("scrollTarget", old);
this.registerListeners();
}
protected updateRootPosition() {
if (this.mdcRoot) {
const windowScroller = this.scrollTarget === window;
// we add support for top-app-bar's tied to an element scroller.
this.mdcRoot.style.position = windowScroller ? "" : "absolute";
}
}
protected barClasses() {
return {
"mdc-top-app-bar--dense": this.dense,
"mdc-top-app-bar--prominent": this.prominent,
"center-title": this.centerTitle,
"mdc-top-app-bar--fixed": true,
"mdc-top-app-bar--pane": this.pane,
};
}
protected contentClasses() {
return {
"mdc-top-app-bar--fixed-adjust": !this.dense && !this.prominent,
"mdc-top-app-bar--dense-fixed-adjust": this.dense && !this.prominent,
"mdc-top-app-bar--prominent-fixed-adjust": !this.dense && this.prominent,
"mdc-top-app-bar--dense-prominent-fixed-adjust":
this.dense && this.prominent,
"mdc-top-app-bar--pane": this.pane,
};
}
protected override render() {
const title = html`<span class="mdc-top-app-bar__title"
><slot name="title"></slot
></span>`;
return html`
<header class="mdc-top-app-bar ${classMap(this.barClasses())}">
<div class="mdc-top-app-bar__row">
${this.pane
? html`<section
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start"
id="title"
>
<slot
name="navigationIcon"
@click=${this.handleNavigationClick}
></slot>
${title}
</section>`
: nothing}
<section class="mdc-top-app-bar__section" id="navigation">
${this.pane
? nothing
: html`<slot
name="navigationIcon"
@click=${this.handleNavigationClick}
></slot
>${title}`}
</section>
<section
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-end"
id="actions"
role="toolbar"
>
<slot name="actionItems"></slot>
</section>
</div>
</header>
<div class=${classMap(this.contentClasses())}>
${this.pane
? html`<div class="pane">
<div class="shadow-container"></div>
<div class="ha-scrollbar">
<slot name="pane"></slot>
</div>
${this.footer
? html`<div class="footer">
<slot name="pane-footer"></slot>
</div>`
: nothing}
</div>`
: nothing}
<div class="main">
${this.pane ? html`<div class="shadow-container"></div>` : nothing}
<div class="content">
<slot></slot>
</div>
</div>
</div>
`;
}
protected updated(changedProperties) {
super.updated(changedProperties);
if (
changedProperties.has("pane") &&
changedProperties.get("pane") !== undefined
) {
this.unregisterListeners();
this.registerListeners();
}
}
protected createAdapter(): MDCTopAppBarAdapter {
return {
...addHasRemoveClass(this.mdcRoot),
setStyle: (prprty: string, value: string) =>
this.mdcRoot.style.setProperty(prprty, value),
getTopAppBarHeight: () => this.mdcRoot.clientHeight,
notifyNavigationIconClicked: () => {
this.dispatchEvent(
new Event(strings.NAVIGATION_EVENT, {
bubbles: true,
cancelable: true,
})
);
},
getViewportScrollY: () =>
this.scrollTarget instanceof Window
? this.scrollTarget.pageYOffset
: this.scrollTarget.scrollTop,
getTotalActionItems: () =>
(this._actionItemsSlot as HTMLSlotElement).assignedNodes({
flatten: true,
}).length,
};
}
protected handleTargetScroll = () => {
this.mdcFoundation.handleTargetScroll();
};
protected handlePaneScroll = (ev) => {
if (ev.target.scrollTop > 0) {
ev.target.parentElement.classList.add("scrolled");
} else {
ev.target.parentElement.classList.remove("scrolled");
}
};
protected handleNavigationClick = () => {
this.mdcFoundation.handleNavigationClick();
};
protected registerListeners() {
if (this.pane) {
this._paneElement!.addEventListener(
"scroll",
this.handlePaneScroll,
passiveEventOptionsIfSupported
);
this._contentElement.addEventListener(
"scroll",
this.handlePaneScroll,
passiveEventOptionsIfSupported
);
return;
}
this.scrollTarget.addEventListener(
"scroll",
this.handleTargetScroll,
passiveEventOptionsIfSupported
);
}
protected unregisterListeners() {
this._paneElement?.removeEventListener("scroll", this.handlePaneScroll);
this._contentElement.removeEventListener("scroll", this.handlePaneScroll);
this.scrollTarget.removeEventListener("scroll", this.handleTargetScroll);
}
protected override firstUpdated() {
super.firstUpdated();
this.updateRootPosition();
this.registerListeners();
}
override disconnectedCallback() {
super.disconnectedCallback();
this.unregisterListeners();
}
static override styles = [
styles,
haStyleScrollbar,
css`
.mdc-top-app-bar__row {
height: var(--header-height);
border-bottom: var(--app-header-border-bottom);
}
.mdc-top-app-bar--fixed-adjust {
padding-top: var(--header-height);
}
.shadow-container {
position: absolute;
top: calc(-1 * var(--header-height));
width: 100%;
height: var(--header-height);
z-index: 1;
transition: box-shadow 200ms linear;
}
.scrolled .shadow-container {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.mdc-top-app-bar {
--mdc-typography-headline6-font-weight: 400;
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
background-color: var(
--app-header-background-color,
var(--mdc-theme-primary)
);
}
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
box-shadow: none;
}
#title {
border-right: 1px solid rgba(255, 255, 255, 0.12);
box-sizing: border-box;
flex: 0 0 var(--sidepane-width, 250px);
width: var(--sidepane-width, 250px);
}
div.mdc-top-app-bar--pane {
display: flex;
height: calc(100vh - var(--header-height));
}
.pane {
border-right: 1px solid var(--divider-color);
box-sizing: border-box;
display: flex;
flex: 0 0 var(--sidepane-width, 250px);
width: var(--sidepane-width, 250px);
flex-direction: column;
position: relative;
}
.pane .ha-scrollbar {
flex: 1;
}
.pane .footer {
border-top: 1px solid var(--divider-color);
}
.main {
min-height: 100%;
}
.mdc-top-app-bar--pane .main {
position: relative;
flex: 1;
height: 100%;
}
.mdc-top-app-bar--pane .content {
height: 100%;
overflow: auto;
}
`,
];
}

View File

@ -298,7 +298,7 @@ export class HatScriptGraph extends LitElement {
.notEnabled=${disabled || config.enabled === false} .notEnabled=${disabled || config.enabled === false}
nofocus nofocus
></hat-graph-node> ></hat-graph-node>
${ensureArray(config.then).map((action, j) => ${ensureArray(config.then ?? []).map((action, j) =>
this.render_action_node( this.render_action_node(
action, action,
`${path}/then/${j}`, `${path}/then/${j}`,

Some files were not shown because too many files have changed in this diff Show More