20240529.0 (#20901)

This commit is contained in:
Bram Kragten 2024-05-29 18:27:40 +02:00 committed by GitHub
commit 1a2daf8a7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
255 changed files with 6867 additions and 6363 deletions

View File

@ -115,6 +115,7 @@
} }
], ],
"unused-imports/no-unused-imports": "error", "unused-imports/no-unused-imports": "error",
"lit/attribute-names": "warn",
"lit/attribute-value-entities": "off", "lit/attribute-value-entities": "off",
"lit/no-template-map": "off", "lit/no-template-map": "off",
"lit/no-native-attributes": "warn", "lit/no-native-attributes": "warn",

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.4 uses: actions/checkout@v4.1.6
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.4 uses: actions/checkout@v4.1.6
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.4 uses: actions/checkout@v4.1.6
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
with: with:
@ -58,7 +58,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.4 uses: actions/checkout@v4.1.6
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
with: with:
@ -76,7 +76,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.4 uses: actions/checkout@v4.1.6
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
with: with:
@ -100,7 +100,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.4 uses: actions/checkout@v4.1.6
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
with: with:

View File

@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.1.4 uses: actions/checkout@v4.1.6
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.4 uses: actions/checkout@v4.1.6
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.4 uses: actions/checkout@v4.1.6
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.4 uses: actions/checkout@v4.1.6
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2

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.4 uses: actions/checkout@v4.1.6
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2

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.4 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5 uses: actions/setup-python@v5

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.4 uses: actions/checkout@v4.1.6
- name: Verify version - name: Verify version
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
@ -55,7 +55,7 @@ jobs:
script/release script/release
- name: Upload release assets - name: Upload release assets
uses: softprops/action-gh-release@v2.0.4 uses: softprops/action-gh-release@v2.0.5
with: with:
files: | files: |
dist/*.whl dist/*.whl

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

File diff suppressed because one or more lines are too long

View File

@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.1.1.cjs yarnPath: .yarn/releases/yarn-4.2.2.cjs

View File

@ -1,7 +1,56 @@
import defineProvider from "@babel/helper-define-polyfill-provider"; import defineProvider from "@babel/helper-define-polyfill-provider";
import { join } from "node:path";
import paths from "../paths.cjs";
const POLYFILL_DIR = join(paths.polymer_dir, "src/resources/polyfills");
// List of polyfill keys with supported browser targets for the functionality // List of polyfill keys with supported browser targets for the functionality
const PolyfillSupport = { const PolyfillSupport = {
// Note states and shadowRoot properties should be supported.
"element-internals": {
android: 90,
chrome: 90,
edge: 90,
firefox: 126,
ios: 17.4,
opera: 76,
opera_mobile: 64,
safari: 17.4,
samsung: 15.0,
},
"element-append": {
android: 54,
chrome: 54,
edge: 17,
firefox: 49,
ios: 10.0,
opera: 41,
opera_mobile: 41,
safari: 10.0,
samsung: 6.0,
},
"element-getattributenames": {
android: 61,
chrome: 61,
edge: 18,
firefox: 45,
ios: 10.3,
opera: 48,
opera_mobile: 45,
safari: 10.1,
samsung: 8.0,
},
"element-toggleattribute": {
android: 69,
chrome: 69,
edge: 18,
firefox: 63,
ios: 12.0,
opera: 56,
opera_mobile: 48,
safari: 12.0,
samsung: 10.0,
},
fetch: { fetch: {
android: 42, android: 42,
chrome: 42, chrome: 42,
@ -13,6 +62,31 @@ const PolyfillSupport = {
safari: 10.1, safari: 10.1,
samsung: 4.0, samsung: 4.0,
}, },
"intl-getcanonicallocales": {
android: 54,
chrome: 54,
edge: 16,
firefox: 48,
ios: 10.3,
opera: 41,
opera_mobile: 41,
safari: 10.1,
samsung: 6.0,
},
"intl-locale": {
android: 74,
chrome: 74,
edge: 79,
firefox: 75,
ios: 14.0,
opera: 62,
opera_mobile: 53,
safari: 14.0,
samsung: 11.0,
},
"intl-other": {
// Not specified (i.e. always try polyfill) since compatibility depends on supported locales
},
proxy: { proxy: {
android: 49, android: 49,
chrome: 49, chrome: 49,
@ -24,17 +98,67 @@ const PolyfillSupport = {
safari: 10.0, safari: 10.0,
samsung: 5.0, samsung: 5.0,
}, },
"resize-observer": {
android: 64,
chrome: 64,
edge: 79,
firefox: 69,
ios: 13.4,
opera: 51,
opera_mobile: 47,
safari: 13.1,
samsung: 9.0,
},
}; };
// Map of global variables and/or instance and static properties to the // Map of global variables and/or instance and static properties to the
// corresponding polyfill key and actual module to import // corresponding polyfill key and actual module to import
const polyfillMap = { const polyfillMap = {
global: { global: {
Proxy: { key: "proxy", module: "proxy-polyfill" },
fetch: { key: "fetch", module: "unfetch/polyfill" }, fetch: { key: "fetch", module: "unfetch/polyfill" },
Proxy: { key: "proxy", module: "proxy-polyfill" },
ResizeObserver: {
key: "resize-observer",
module: join(POLYFILL_DIR, "resize-observer.ts"),
},
},
instance: {
attachInternals: {
key: "element-internals",
module: "element-internals-polyfill",
},
...Object.fromEntries(
["append", "getAttributeNames", "toggleAttribute"].map((prop) => {
const key = `element-${prop.toLowerCase()}`;
return [prop, { key, module: join(POLYFILL_DIR, `${key}.ts`) }];
})
),
},
static: {
Intl: {
getCanonicalLocales: {
key: "intl-getcanonicallocales",
module: join(POLYFILL_DIR, "intl-polyfill.ts"),
},
Locale: {
key: "intl-locale",
module: join(POLYFILL_DIR, "intl-polyfill.ts"),
},
...Object.fromEntries(
[
"DateTimeFormat",
"DisplayNames",
"ListFormat",
"NumberFormat",
"PluralRules",
"RelativeTimeFormat",
].map((obj) => [
obj,
{ key: "intl-other", module: join(POLYFILL_DIR, "intl-polyfill.ts") },
])
),
},
}, },
instance: {},
static: {},
}; };
// Create plugin using the same factory as for CoreJS // Create plugin using the same factory as for CoreJS
@ -42,14 +166,16 @@ export default defineProvider(
({ createMetaResolver, debug, shouldInjectPolyfill }) => { ({ createMetaResolver, debug, shouldInjectPolyfill }) => {
const resolvePolyfill = createMetaResolver(polyfillMap); const resolvePolyfill = createMetaResolver(polyfillMap);
return { return {
name: "HA Custom", name: "custom-polyfill",
polyfills: PolyfillSupport, polyfills: PolyfillSupport,
usageGlobal(meta, utils) { usageGlobal(meta, utils) {
const polyfill = resolvePolyfill(meta); const polyfill = resolvePolyfill(meta);
if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) { if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) {
debug(polyfill.desc.key); debug(polyfill.desc.key);
utils.injectGlobalImport(polyfill.desc.module); utils.injectGlobalImport(polyfill.desc.module);
return true;
} }
return false;
}, },
}; };
} }

View File

@ -3,6 +3,8 @@ const env = require("./env.cjs");
const paths = require("./paths.cjs"); const paths = require("./paths.cjs");
const { dependencies } = require("../package.json"); const { dependencies } = require("../package.json");
const BABEL_PLUGINS = path.join(__dirname, "babel-plugins");
// GitHub base URL to use for production source maps // GitHub base URL to use for production source maps
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version // Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
module.exports.sourceMapURL = () => { module.exports.sourceMapURL = () => {
@ -100,22 +102,12 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
], ],
plugins: [ plugins: [
[ [
path.resolve( path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"),
paths.polymer_dir,
"build-scripts/babel-plugins/inline-constants-plugin.cjs"
),
{ {
modules: ["@mdi/js"], modules: ["@mdi/js"],
ignoreModuleNotFound: true, ignoreModuleNotFound: true,
}, },
], ],
[
path.resolve(
paths.polymer_dir,
"build-scripts/babel-plugins/custom-polyfill-plugin.js"
),
{ method: "usage-global" },
],
// Minify template literals for production // Minify template literals for production
isProdBuild && [ isProdBuild && [
"template-html-minifier", "template-html-minifier",
@ -153,6 +145,27 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
], ],
sourceMaps: !isTestBuild, sourceMaps: !isTestBuild,
overrides: [ overrides: [
{
// Add plugin to inject various polyfills, excluding the polyfills
// themselves to prevent self-injection.
plugins: [
[
path.join(BABEL_PLUGINS, "custom-polyfill-plugin.js"),
{ method: "usage-global" },
],
],
exclude: [
path.join(paths.polymer_dir, "src/resources/polyfills"),
...[
"@formatjs/intl-\\w+",
"@lit-labs/virtualizer/polyfills",
"@webcomponents/scoped-custom-element-registry",
"element-internals-polyfill",
"proxy-polyfill",
"unfetch",
].map((p) => new RegExp(`/node_modules/${p}/`)),
],
},
{ {
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files // Use unambiguous for dependencies so that require() is correctly injected into CommonJS files
// Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills // Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills

View File

@ -1,12 +1,14 @@
/* eslint-disable max-classes-per-file */
import { deleteAsync } from "del"; import { deleteAsync } from "del";
import { glob } from "glob"; import { glob } from "glob";
import gulp from "gulp"; import gulp from "gulp";
import merge from "gulp-merge-json";
import rename from "gulp-rename"; import rename from "gulp-rename";
import merge from "lodash.merge";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { mkdir, readFile } from "node:fs/promises"; import { mkdir, readFile } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { Transform } from "node:stream"; import { PassThrough, Transform } from "node:stream";
import { finished } from "node:stream/promises"; import { finished } from "node:stream/promises";
import env from "../env.cjs"; import env from "../env.cjs";
import paths from "../paths.cjs"; import paths from "../paths.cjs";
@ -17,6 +19,7 @@ const inBackendDir = "translations/backend";
const workDir = "build/translations"; const workDir = "build/translations";
const outDir = join(workDir, "output"); const outDir = join(workDir, "output");
const EN_SRC = join(paths.translations_src, "en.json"); const EN_SRC = join(paths.translations_src, "en.json");
const TEST_LOCALE = "en-x-test";
let mergeBackend = false; let mergeBackend = false;
@ -54,6 +57,39 @@ class CustomJSON extends Transform {
} }
} }
// Transform stream to merge Vinyl JSON files (buffer mode only).
class MergeJSON extends Transform {
_objects = [];
constructor(stem, startObj = {}, reviver = null) {
super({ objectMode: true, allowHalfOpen: false });
this._stem = stem;
this._startObj = structuredClone(startObj);
this._reviver = reviver;
}
async _transform(file, _, callback) {
try {
this._objects.push(JSON.parse(file.contents.toString(), this._reviver));
if (!this._outFile) this._outFile = file.clone({ contents: false });
callback(null);
} catch (err) {
callback(err);
}
}
async _flush(callback) {
try {
const mergedObj = merge(this._startObj, ...this._objects);
this._outFile.contents = Buffer.from(JSON.stringify(mergedObj));
this._outFile.stem = this._stem;
callback(null, this._outFile);
} catch (err) {
callback(err);
}
}
}
// Utility to flatten object keys to single level using separator // Utility to flatten object keys to single level using separator
const flatten = (data, prefix = "", sep = ".") => { const flatten = (data, prefix = "", sep = ".") => {
const output = {}; const output = {};
@ -115,7 +151,7 @@ const createTestTranslation = () =>
: gulp : gulp
.src(EN_SRC) .src(EN_SRC)
.pipe(new CustomJSON(null, testReviver)) .pipe(new CustomJSON(null, testReviver))
.pipe(rename("test.json")) .pipe(rename(`${TEST_LOCALE}.json`))
.pipe(gulp.dest(workDir)); .pipe(gulp.dest(workDir));
/** /**
@ -131,12 +167,7 @@ const createMasterTranslation = () =>
gulp gulp
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])]) .src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
.pipe(new CustomJSON(lokaliseTransform)) .pipe(new CustomJSON(lokaliseTransform))
.pipe( .pipe(new MergeJSON("en"))
merge({
fileName: "en.json",
jsonSpace: undefined,
})
)
.pipe(gulp.dest(workDir)); .pipe(gulp.dest(workDir));
const FRAGMENTS = ["base"]; const FRAGMENTS = ["base"];
@ -162,7 +193,7 @@ const createTranslations = async () => {
// each locale, then fragmentizes and flattens the data for final output. // each locale, then fragmentizes and flattens the data for final output.
const translationFiles = await glob([ const translationFiles = await glob([
`${inFrontendDir}/!(en).json`, `${inFrontendDir}/!(en).json`,
...(env.isProdBuild() ? [] : [`${workDir}/test.json`]), ...(env.isProdBuild() ? [] : [`${workDir}/${TEST_LOCALE}.json`]),
]); ]);
const hashStream = new Transform({ const hashStream = new Transform({
objectMode: true, objectMode: true,
@ -213,7 +244,10 @@ const createTranslations = async () => {
// TODO: This is a naive interpretation of BCP47 that should be improved. // TODO: This is a naive interpretation of BCP47 that should be improved.
// Will be OK for now as long as we don't have anything more complicated // Will be OK for now as long as we don't have anything more complicated
// than a base translation + region. // than a base translation + region.
gulp.src(`${workDir}/en.json`).pipe(hashStream, { end: false }); gulp
.src(`${workDir}/en.json`)
.pipe(new PassThrough({ objectMode: true }))
.pipe(hashStream, { end: false });
const mergesFinished = []; const mergesFinished = [];
for (const translationFile of translationFiles) { for (const translationFile of translationFiles) {
const locale = basename(translationFile, ".json"); const locale = basename(translationFile, ".json");
@ -221,8 +255,8 @@ const createTranslations = async () => {
const mergeFiles = []; const mergeFiles = [];
for (let i = 1; i <= subtags.length; i++) { for (let i = 1; i <= subtags.length; i++) {
const lang = subtags.slice(0, i).join("-"); const lang = subtags.slice(0, i).join("-");
if (lang === "test") { if (lang === TEST_LOCALE) {
mergeFiles.push(`${workDir}/test.json`); mergeFiles.push(`${workDir}/${TEST_LOCALE}.json`);
} else if (lang !== "en") { } else if (lang !== "en") {
mergeFiles.push(`${inFrontendDir}/${lang}.json`); mergeFiles.push(`${inFrontendDir}/${lang}.json`);
if (mergeBackend) { if (mergeBackend) {
@ -230,14 +264,9 @@ const createTranslations = async () => {
} }
} }
} }
const mergeStream = gulp.src(mergeFiles, { allowEmpty: true }).pipe( const mergeStream = gulp
merge({ .src(mergeFiles, { allowEmpty: true })
fileName: `${locale}.json`, .pipe(new MergeJSON(locale, enMaster, emptyReviver));
startObj: enMaster,
jsonReviver: emptyReviver,
jsonSpace: undefined,
})
);
mergesFinished.push(finished(mergeStream)); mergesFinished.push(finished(mergeStream));
mergeStream.pipe(hashStream, { end: false }); mergeStream.pipe(hashStream, { end: false });
} }
@ -256,7 +285,7 @@ const writeTranslationMetaData = () =>
new CustomJSON((meta) => { new CustomJSON((meta) => {
// Add the test translation in development. // Add the test translation in development.
if (!env.isProdBuild()) { if (!env.isProdBuild()) {
meta.test = { nativeName: "Test" }; meta[TEST_LOCALE] = { nativeName: "Translation Test" };
} }
// Filter out locales without a native name, and add the hashes. // Filter out locales without a native name, and add the hashes.
for (const locale of Object.keys(meta)) { for (const locale of Object.keys(meta)) {

View File

@ -10,6 +10,7 @@ const WebpackBar = require("webpackbar");
const { const {
TransformAsyncModulesPlugin, TransformAsyncModulesPlugin,
} = require("transform-async-modules-webpack-plugin"); } = require("transform-async-modules-webpack-plugin");
const { dependencies } = require("../package.json");
const paths = require("./paths.cjs"); const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs"); const bundle = require("./bundle.cjs");
@ -156,7 +157,10 @@ const createWebpackConfig = ({
transform: (stats) => JSON.stringify(filterStats(stats)), transform: (stats) => JSON.stringify(filterStats(stats)),
}), }),
!latestBuild && !latestBuild &&
new TransformAsyncModulesPlugin({ browserslistEnv: "legacy" }), new TransformAsyncModulesPlugin({
browserslistEnv: "legacy",
runtime: { version: dependencies["@babel/runtime"] },
}),
].filter(Boolean), ].filter(Boolean),
resolve: { resolve: {
extensions: [".ts", ".js", ".json"], extensions: [".ts", ".js", ".json"],

View File

@ -1,5 +1,5 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiCast, mdiCastConnected } from "@mdi/js"; import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import { Auth, Connection } from "home-assistant-js-websocket"; import { Auth, Connection } from "home-assistant-js-websocket";
@ -104,8 +104,11 @@ class HcCast extends LitElement {
slot="item-icon" slot="item-icon"
></ha-icon> ></ha-icon>
` `
: ""} : html`<ha-svg-icon
${view.title || view.path} slot="item-icon"
.path=${mdiViewDashboard}
></ha-svg-icon>`}
${view.title || view.path || "Unnamed view"}
</paper-icon-item> </paper-icon-item>
` `
)} )}
@ -250,7 +253,8 @@ class HcCast extends LitElement {
padding-top: 0; padding-top: 0;
} }
paper-listbox ha-icon { paper-listbox ha-icon,
paper-listbox ha-svg-icon {
padding: 12px; padding: 12px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }

View File

@ -2,6 +2,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types"; import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
import { Lovelace } from "../../../../src/panels/lovelace/types"; import { Lovelace } from "../../../../src/panels/lovelace/types";
import "../../../../src/panels/lovelace/views/hui-view"; import "../../../../src/panels/lovelace/views/hui-view";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
@ -61,7 +62,12 @@ class HcLovelace extends LitElement {
const index = this._viewIndex; const index = this._viewIndex;
if (index !== undefined) { if (index !== undefined) {
const dashboardTitle = this.lovelaceConfig.title || this.urlPath; const title = getPanelTitleFromUrlPath(
this.hass,
this.urlPath || "lovelace"
);
const dashboardTitle = title || this.urlPath;
const viewTitle = const viewTitle =
this.lovelaceConfig.views[index].title || this.lovelaceConfig.views[index].title ||
@ -80,10 +86,17 @@ class HcLovelace extends LitElement {
this.lovelaceConfig.views[index].background || this.lovelaceConfig.views[index].background ||
this.lovelaceConfig.background; this.lovelaceConfig.background;
if (configBackground) { const backgroundStyle =
typeof configBackground === "string"
? configBackground
: configBackground?.image
? `center / cover no-repeat url('${configBackground.image}')`
: undefined;
if (backgroundStyle) {
this._huiView!.style.setProperty( this._huiView!.style.setProperty(
"--lovelace-background", "--lovelace-background",
configBackground backgroundStyle
); );
} else { } else {
this._huiView!.style.removeProperty("--lovelace-background"); this._huiView!.style.removeProperty("--lovelace-background");

View File

@ -35,6 +35,7 @@ import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/lo
import { HassElement } from "../../../../src/state/hass-element"; import { HassElement } from "../../../../src/state/hass-element";
import { castContext } from "../cast_context"; import { castContext } from "../cast_context";
import "./hc-launch-screen"; import "./hc-launch-screen";
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = { const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
strategy: { strategy: {
@ -359,7 +360,11 @@ export class HcMain extends HassElement {
} }
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) { private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
castContext.setApplicationState(lovelaceConfig.title || ""); const title = getPanelTitleFromUrlPath(
this.hass!,
this._urlPath || "lovelace"
);
castContext.setApplicationState(title || "");
this._lovelaceConfig = lovelaceConfig; this._lovelaceConfig = lovelaceConfig;
} }

View File

@ -64,6 +64,12 @@ const ACTIONS = [
entity_id: "input_boolean.toggle_4", entity_id: "input_boolean.toggle_4",
}, },
}, },
{
sequence: [
{ scene: "scene.kitchen_morning" },
{ service: "light.turn_off", target: { entity_id: "light.kitchen" } },
],
},
{ {
parallel: [ parallel: [
{ scene: "scene.kitchen_morning" }, { scene: "scene.kitchen_morning" },

View File

@ -20,6 +20,7 @@ import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation
import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template"; import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
import { Action } from "../../../../src/data/script"; import { Action } from "../../../../src/data/script";
import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition"; import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition";
import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence";
import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel"; import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel";
import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if"; import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if";
import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop"; import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop";
@ -39,6 +40,7 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "If-Then", actions: [HaIfAction.defaultConfig] }, { name: "If-Then", actions: [HaIfAction.defaultConfig] },
{ name: "Choose", actions: [HaChooseAction.defaultConfig] }, { name: "Choose", actions: [HaChooseAction.defaultConfig] },
{ name: "Variables", actions: [{ variables: { hello: "1" } }] }, { name: "Variables", actions: [{ variables: { hello: "1" } }] },
{ name: "Sequence", actions: [HaSequenceAction.defaultConfig] },
{ name: "Parallel", actions: [HaParallelAction.defaultConfig] }, { name: "Parallel", actions: [HaParallelAction.defaultConfig] },
{ name: "Stop", actions: [HaStopAction.defaultConfig] }, { name: "Stop", actions: [HaStopAction.defaultConfig] },
]; ];

View File

@ -56,48 +56,46 @@ export class DemoDateTimeDateTimeNumeric extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations) ${Object.entries(translationMetadata.translations).map(
.filter(([key, _]) => key !== "test") ([key, value]) => html`
.map( <div class="container">
([key, value]) => html` <div>${value.nativeName}</div>
<div class="container"> <div class="center">
<div>${value.nativeName}</div> ${formatDateTimeNumeric(
<div class="center"> this.date,
${formatDateTimeNumeric( {
this.date, ...defaultLocale,
{ language: key,
...defaultLocale, time_format: TimeFormat.language,
language: key, },
time_format: TimeFormat.language, demoConfig
}, )}
demoConfig
)}
</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
` <div class="center">
)} ${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@ -56,48 +56,46 @@ export class DemoDateTimeDateTimeSeconds extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations) ${Object.entries(translationMetadata.translations).map(
.filter(([key, _]) => key !== "test") ([key, value]) => html`
.map( <div class="container">
([key, value]) => html` <div>${value.nativeName}</div>
<div class="container"> <div class="center">
<div>${value.nativeName}</div> ${formatDateTimeWithSeconds(
<div class="center"> this.date,
${formatDateTimeWithSeconds( {
this.date, ...defaultLocale,
{ language: key,
...defaultLocale, time_format: TimeFormat.language,
language: key, },
time_format: TimeFormat.language, demoConfig
}, )}
demoConfig
)}
</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
` <div class="center">
)} ${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@ -56,48 +56,46 @@ export class DemoDateTimeDateTimeShortYear extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations) ${Object.entries(translationMetadata.translations).map(
.filter(([key, _]) => key !== "test") ([key, value]) => html`
.map( <div class="container">
([key, value]) => html` <div>${value.nativeName}</div>
<div class="container"> <div class="center">
<div>${value.nativeName}</div> ${formatShortDateTimeWithYear(
<div class="center"> this.date,
${formatShortDateTimeWithYear( {
this.date, ...defaultLocale,
{ language: key,
...defaultLocale, time_format: TimeFormat.language,
language: key, },
time_format: TimeFormat.language, demoConfig
}, )}
demoConfig
)}
</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
` <div class="center">
)} ${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@ -56,48 +56,46 @@ export class DemoDateTimeDateTimeShort extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations) ${Object.entries(translationMetadata.translations).map(
.filter(([key, _]) => key !== "test") ([key, value]) => html`
.map( <div class="container">
([key, value]) => html` <div>${value.nativeName}</div>
<div class="container"> <div class="center">
<div>${value.nativeName}</div> ${formatShortDateTime(
<div class="center"> this.date,
${formatShortDateTime( {
this.date, ...defaultLocale,
{ language: key,
...defaultLocale, time_format: TimeFormat.language,
language: key, },
time_format: TimeFormat.language, demoConfig
}, )}
demoConfig
)}
</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
` <div class="center">
)} ${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@ -56,48 +56,46 @@ export class DemoDateTimeDateTime extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations) ${Object.entries(translationMetadata.translations).map(
.filter(([key, _]) => key !== "test") ([key, value]) => html`
.map( <div class="container">
([key, value]) => html` <div>${value.nativeName}</div>
<div class="container"> <div class="center">
<div>${value.nativeName}</div> ${formatDateTime(
<div class="center"> this.date,
${formatDateTime( {
this.date, ...defaultLocale,
{ language: key,
...defaultLocale, time_format: TimeFormat.language,
language: key, },
time_format: TimeFormat.language, demoConfig
}, )}
demoConfig
)}
</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
` <div class="center">
)} ${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@ -35,59 +35,57 @@ export class DemoDateTimeDate extends LitElement {
<div class="center">Month-Day-Year</div> <div class="center">Month-Day-Year</div>
<div class="center">Year-Month-Day</div> <div class="center">Year-Month-Day</div>
</div> </div>
${Object.entries(translationMetadata.translations) ${Object.entries(translationMetadata.translations).map(
.filter(([key, _]) => key !== "test") ([key, value]) => html`
.map( <div class="container">
([key, value]) => html` <div>${value.nativeName}</div>
<div class="container"> <div class="center">
<div>${value.nativeName}</div> ${formatDateNumeric(
<div class="center"> date,
${formatDateNumeric( {
date, ...defaultLocale,
{ language: key,
...defaultLocale, date_format: DateFormat.language,
language: key, },
date_format: DateFormat.language, demoConfig
}, )}
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.DMY,
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.MDY,
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.YMD,
},
demoConfig
)}
</div>
</div> </div>
` <div class="center">
)} ${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.DMY,
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.MDY,
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.YMD,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@ -56,48 +56,46 @@ export class DemoDateTimeTimeSeconds extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations) ${Object.entries(translationMetadata.translations).map(
.filter(([key, _]) => key !== "test") ([key, value]) => html`
.map( <div class="container">
([key, value]) => html` <div>${value.nativeName}</div>
<div class="container"> <div class="center">
<div>${value.nativeName}</div> ${formatTimeWithSeconds(
<div class="center"> this.date,
${formatTimeWithSeconds( {
this.date, ...defaultLocale,
{ language: key,
...defaultLocale, time_format: TimeFormat.language,
language: key, },
time_format: TimeFormat.language, demoConfig
}, )}
demoConfig
)}
</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
` <div class="center">
)} ${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@ -56,48 +56,46 @@ export class DemoDateTimeTimeWeekday extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations) ${Object.entries(translationMetadata.translations).map(
.filter(([key, _]) => key !== "test") ([key, value]) => html`
.map( <div class="container">
([key, value]) => html` <div>${value.nativeName}</div>
<div class="container"> <div class="center">
<div>${value.nativeName}</div> ${formatTimeWeekday(
<div class="center"> this.date,
${formatTimeWeekday( {
this.date, ...defaultLocale,
{ language: key,
...defaultLocale, time_format: TimeFormat.language,
language: key, },
time_format: TimeFormat.language, demoConfig
}, )}
demoConfig
)}
</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
` <div class="center">
)} ${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@ -56,48 +56,46 @@ export class DemoDateTimeTime extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations) ${Object.entries(translationMetadata.translations).map(
.filter(([key, _]) => key !== "test") ([key, value]) => html`
.map( <div class="container">
([key, value]) => html` <div>${value.nativeName}</div>
<div class="container"> <div class="center">
<div>${value.nativeName}</div> ${formatTime(
<div class="center"> this.date,
${formatTime( {
this.date, ...defaultLocale,
{ language: key,
...defaultLocale, time_format: TimeFormat.language,
language: key, },
time_format: TimeFormat.language, demoConfig
}, )}
demoConfig
)}
</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
` <div class="center">
)} ${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@ -368,6 +368,7 @@ export class DemoEntityState extends LitElement {
hass.localize, hass.localize,
entry.stateObj, entry.stateObj,
hass.locale, hass.locale,
[], // numericDeviceClasses
hass.config, hass.config,
hass.entities hass.entities
)}`, )}`,

View File

@ -25,8 +25,8 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.24.4", "@babel/runtime": "7.24.6",
"@braintree/sanitize-url": "7.0.1", "@braintree/sanitize-url": "7.0.2",
"@codemirror/autocomplete": "6.16.0", "@codemirror/autocomplete": "6.16.0",
"@codemirror/commands": "6.5.0", "@codemirror/commands": "6.5.0",
"@codemirror/language": "6.10.1", "@codemirror/language": "6.10.1",
@ -35,20 +35,20 @@
"@codemirror/state": "6.4.1", "@codemirror/state": "6.4.1",
"@codemirror/view": "6.26.3", "@codemirror/view": "6.26.3",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.3", "@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.6", "@formatjs/intl-displaynames": "6.6.8",
"@formatjs/intl-getcanonicallocales": "2.3.0", "@formatjs/intl-getcanonicallocales": "2.3.0",
"@formatjs/intl-listformat": "7.5.5", "@formatjs/intl-listformat": "7.5.7",
"@formatjs/intl-locale": "3.4.5", "@formatjs/intl-locale": "4.0.0",
"@formatjs/intl-numberformat": "8.10.1", "@formatjs/intl-numberformat": "8.10.3",
"@formatjs/intl-pluralrules": "5.2.12", "@formatjs/intl-pluralrules": "5.2.14",
"@formatjs/intl-relativetimeformat": "11.2.12", "@formatjs/intl-relativetimeformat": "11.2.14",
"@fullcalendar/core": "6.1.11", "@fullcalendar/core": "6.1.13",
"@fullcalendar/daygrid": "6.1.11", "@fullcalendar/daygrid": "6.1.13",
"@fullcalendar/interaction": "6.1.11", "@fullcalendar/interaction": "6.1.13",
"@fullcalendar/list": "6.1.11", "@fullcalendar/list": "6.1.13",
"@fullcalendar/luxon3": "6.1.11", "@fullcalendar/luxon3": "6.1.13",
"@fullcalendar/timegrid": "6.1.11", "@fullcalendar/timegrid": "6.1.13",
"@lezer/highlight": "1.2.0", "@lezer/highlight": "1.2.0",
"@lit-labs/context": "0.4.1", "@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.7", "@lit-labs/motion": "1.0.7",
@ -70,7 +70,6 @@
"@material/mwc-list": "0.27.0", "@material/mwc-list": "0.27.0",
"@material/mwc-menu": "0.27.0", "@material/mwc-menu": "0.27.0",
"@material/mwc-radio": "0.27.0", "@material/mwc-radio": "0.27.0",
"@material/mwc-ripple": "0.27.0",
"@material/mwc-select": "0.27.0", "@material/mwc-select": "0.27.0",
"@material/mwc-snackbar": "0.27.0", "@material/mwc-snackbar": "0.27.0",
"@material/mwc-switch": "0.27.0", "@material/mwc-switch": "0.27.0",
@ -81,7 +80,7 @@
"@material/mwc-top-app-bar": "0.27.0", "@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0", "@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "1.4.1", "@material/web": "1.5.0",
"@mdi/js": "7.4.47", "@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47", "@mdi/svg": "7.4.47",
"@polymer/paper-item": "3.0.1", "@polymer/paper-item": "3.0.1",
@ -89,8 +88,8 @@
"@polymer/paper-tabs": "3.1.0", "@polymer/paper-tabs": "3.1.0",
"@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.3.11", "@vaadin/combo-box": "24.3.13",
"@vaadin/vaadin-themable-mixin": "24.3.11", "@vaadin/vaadin-themable-mixin": "24.3.13",
"@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",
@ -98,10 +97,10 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9", "@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0", "@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1", "app-datepicker": "5.1.1",
"chart.js": "4.4.2", "chart.js": "4.4.3",
"color-name": "2.0.0", "color-name": "2.0.0",
"comlink": "4.4.1", "comlink": "4.4.1",
"core-js": "3.37.0", "core-js": "3.37.1",
"cropperjs": "1.6.2", "cropperjs": "1.6.2",
"date-fns": "3.6.0", "date-fns": "3.6.0",
"date-fns-tz": "3.1.3", "date-fns-tz": "3.1.3",
@ -113,7 +112,7 @@
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch", "hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.3.0", "home-assistant-js-websocket": "9.3.0",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"intl-messageformat": "10.5.11", "intl-messageformat": "10.5.14",
"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",
@ -150,33 +149,34 @@
"xss": "1.0.15" "xss": "1.0.15"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.24.4", "@babel/core": "7.24.6",
"@babel/helper-define-polyfill-provider": "0.6.2", "@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.24.1", "@babel/plugin-proposal-decorators": "7.24.6",
"@babel/plugin-transform-runtime": "7.24.3", "@babel/plugin-transform-runtime": "7.24.6",
"@babel/preset-env": "7.24.4", "@babel/preset-env": "7.24.6",
"@babel/preset-typescript": "7.24.1", "@babel/preset-typescript": "7.24.6",
"@bundle-stats/plugin-webpack-filter": "4.12.2", "@bundle-stats/plugin-webpack-filter": "4.13.2",
"@koa/cors": "5.0.0", "@koa/cors": "5.0.0",
"@lokalise/node-api": "12.4.1", "@lokalise/node-api": "12.5.0",
"@octokit/auth-oauth-device": "7.1.1", "@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.1", "@octokit/plugin-retry": "7.1.1",
"@octokit/rest": "20.1.0", "@octokit/rest": "20.1.1",
"@open-wc/dev-server-hmr": "0.1.4", "@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4", "@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "25.0.7", "@rollup/plugin-commonjs": "25.0.8",
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "15.2.3", "@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.5", "@rollup/plugin-replace": "5.0.5",
"@types/babel__plugin-transform-runtime": "7.9.5", "@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.14", "@types/chromecast-caf-receiver": "6.0.14",
"@types/chromecast-caf-sender": "1.0.9", "@types/chromecast-caf-sender": "1.0.10",
"@types/color-name": "1.1.4", "@types/color-name": "1.1.4",
"@types/glob": "8.1.0", "@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2", "@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.12", "@types/leaflet": "1.9.12",
"@types/leaflet-draw": "1.0.11", "@types/leaflet-draw": "1.0.11",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2", "@types/luxon": "3.4.2",
"@types/mocha": "10.0.6", "@types/mocha": "10.0.6",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
@ -185,13 +185,13 @@
"@types/tar": "6.1.13", "@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39", "@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "7.7.1", "@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.7.1", "@typescript-eslint/parser": "7.10.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": "5.1.0", "chai": "5.1.1",
"del": "7.1.0", "del": "7.1.0",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
@ -200,24 +200,24 @@
"eslint-import-resolver-webpack": "0.13.8", "eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-disable": "2.0.3", "eslint-plugin-disable": "2.0.3",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"eslint-plugin-lit": "1.11.0", "eslint-plugin-lit": "1.13.0",
"eslint-plugin-lit-a11y": "4.1.2", "eslint-plugin-lit-a11y": "4.1.2",
"eslint-plugin-unused-imports": "3.1.0", "eslint-plugin-unused-imports": "4.0.0",
"eslint-plugin-wc": "2.1.0", "eslint-plugin-wc": "2.1.0",
"fancy-log": "2.0.0", "fancy-log": "2.0.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"glob": "10.3.12", "glob": "10.4.1",
"gulp": "4.0.2", "gulp": "5.0.0",
"gulp-json-transform": "0.5.0", "gulp-json-transform": "0.5.0",
"gulp-merge-json": "2.2.1",
"gulp-rename": "2.0.0", "gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.1", "gulp-zopfli-green": "6.0.1",
"html-minifier-terser": "7.2.0", "html-minifier-terser": "7.2.0",
"husky": "9.0.11", "husky": "9.0.11",
"instant-mocha": "1.5.2", "instant-mocha": "1.5.2",
"jszip": "3.10.1", "jszip": "3.10.1",
"lint-staged": "15.2.2", "lint-staged": "15.2.5",
"lit-analyzer": "2.0.3", "lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0", "lodash.template": "4.5.0",
"magic-string": "0.30.10", "magic-string": "0.30.10",
"map-stream": "0.0.7", "map-stream": "0.0.7",
@ -231,12 +231,12 @@
"rollup-plugin-terser": "7.0.2", "rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0", "rollup-plugin-visualizer": "5.12.0",
"serve-handler": "6.1.5", "serve-handler": "6.1.5",
"sinon": "17.0.1", "sinon": "18.0.0",
"source-map-url": "0.4.1", "source-map-url": "0.4.1",
"systemjs": "6.14.3", "systemjs": "6.15.1",
"tar": "7.0.1", "tar": "7.1.0",
"terser-webpack-plugin": "5.3.10", "terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.0", "transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2", "ts-lit-plugin": "2.0.2",
"typescript": "5.4.5", "typescript": "5.4.5",
"webpack": "5.91.0", "webpack": "5.91.0",
@ -257,5 +257,5 @@
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch", "sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.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@4.1.1" "packageManager": "yarn@4.2.2"
} }

View File

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

@ -1,8 +1,6 @@
import { getWeekStartByLocale } from "weekstart"; import { getWeekStartByLocale } from "weekstart";
import { FrontendLocaleData, FirstWeekday } from "../../data/translation"; import { FrontendLocaleData, FirstWeekday } from "../../data/translation";
import "../../resources/intl-polyfill";
export const weekdays = [ export const weekdays = [
"sunday", "sunday",
"monday", "monday",

View File

@ -1,7 +1,6 @@
import { HassConfig } from "home-assistant-js-websocket"; import { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { DateFormat, FrontendLocaleData } from "../../data/translation"; import { DateFormat, FrontendLocaleData } from "../../data/translation";
import "../../resources/intl-polyfill";
import { resolveTimeZone } from "./resolve-time-zone"; import { resolveTimeZone } from "./resolve-time-zone";
// Tuesday, August 10 // Tuesday, August 10

View File

@ -1,7 +1,6 @@
import { HassConfig } from "home-assistant-js-websocket"; import { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData } from "../../data/translation";
import "../../resources/intl-polyfill";
import { formatDateNumeric } from "./format_date"; import { formatDateNumeric } from "./format_date";
import { formatTime } from "./format_time"; import { formatTime } from "./format_time";
import { resolveTimeZone } from "./resolve-time-zone"; import { resolveTimeZone } from "./resolve-time-zone";

View File

@ -1,6 +1,5 @@
import { HaDurationData } from "../../components/ha-duration-input"; import { HaDurationData } from "../../components/ha-duration-input";
import { FrontendLocaleData } from "../../data/translation"; 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);

View File

@ -1,7 +1,6 @@
import { HassConfig } from "home-assistant-js-websocket"; import { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData } from "../../data/translation";
import "../../resources/intl-polyfill";
import { resolveTimeZone } from "./resolve-time-zone"; import { resolveTimeZone } from "./resolve-time-zone";
import { useAmPm } from "./use_am_pm"; import { useAmPm } from "./use_am_pm";

View File

@ -1,5 +1,4 @@
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import "../../resources/intl-polyfill";
export const localizeWeekdays = memoizeOne( export const localizeWeekdays = memoizeOne(
(language: string, short: boolean): string[] => { (language: string, short: boolean): string[] => {

View File

@ -1,6 +1,5 @@
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData } from "../../data/translation";
import "../../resources/intl-polyfill";
import { selectUnit } from "../util/select-unit"; import { selectUnit } from "../util/select-unit";
const formatRelTimeMem = memoizeOne( const formatRelTimeMem = memoizeOne(

View File

@ -108,6 +108,8 @@ export const storage =
subscribe?: boolean; subscribe?: boolean;
state?: boolean; state?: boolean;
stateOptions?: InternalPropertyDeclaration; stateOptions?: InternalPropertyDeclaration;
serializer?: (value: any) => any;
deserializer?: (value: any) => any;
}): any => }): any =>
(clsElement: ClassElement) => { (clsElement: ClassElement) => {
const storageName = options.storage || "localStorage"; const storageName = options.storage || "localStorage";
@ -141,7 +143,9 @@ export const storage =
const getValue = (): any => const getValue = (): any =>
storageInstance.hasKey(storageKey!) storageInstance.hasKey(storageKey!)
? storageInstance.getValue(storageKey!) ? options.deserializer
? options.deserializer(storageInstance.getValue(storageKey!))
: storageInstance.getValue(storageKey!)
: initVal; : initVal;
const setValue = (el: ReactiveElement, value: any) => { const setValue = (el: ReactiveElement, value: any) => {
@ -149,7 +153,10 @@ export const storage =
if (options.state) { if (options.state) {
oldValue = getValue(); oldValue = getValue();
} }
storageInstance.setValue(storageKey!, value); storageInstance.setValue(
storageKey!,
options.serializer ? options.serializer(value) : value
);
if (options.state) { if (options.state) {
el.requestUpdate(clsElement.key, oldValue); el.requestUpdate(clsElement.key, oldValue);
} }

View File

@ -61,8 +61,11 @@ export const applyThemesOnElement = (
const accentColor = themeSettings?.accentColor; const accentColor = themeSettings?.accentColor;
if (darkMode && primaryColor) { if (darkMode && primaryColor) {
themeRules["app-theme-color"] = hexBlend(primaryColor, "#121212", 8); themeRules["app-header-background-color"] = hexBlend(
themeRules["app-header-background-color"] = themeRules["app-theme-color"]; primaryColor,
"#121212",
8
);
} }
if (primaryColor) { if (primaryColor) {

View File

@ -1,3 +1,5 @@
export type MediaQueriesListener = () => void;
/** /**
* Attach a media query. Listener is called right away and when it matches. * Attach a media query. Listener is called right away and when it matches.
* @param mediaQuery media query to match. * @param mediaQuery media query to match.
@ -7,7 +9,7 @@
export const listenMediaQuery = ( export const listenMediaQuery = (
mediaQuery: string, mediaQuery: string,
matchesChanged: (matches: boolean) => void matchesChanged: (matches: boolean) => void
) => { ): MediaQueriesListener => {
const mql = matchMedia(mediaQuery); const mql = matchMedia(mediaQuery);
const listener = (e) => matchesChanged(e.matches); const listener = (e) => matchesChanged(e.matches);
mql.addListener(listener); mql.addListener(listener);

View File

@ -19,28 +19,11 @@ import { blankBeforeUnit } from "../translations/blank_before_unit";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
export const computeStateDisplaySingleEntity = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendLocaleData,
config: HassConfig,
entity: EntityRegistryDisplayEntry | undefined,
state?: string
): string =>
computeStateDisplayFromEntityAttributes(
localize,
locale,
config,
entity,
stateObj.entity_id,
stateObj.attributes,
state !== undefined ? state : stateObj.state
);
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
stateObj: HassEntity, stateObj: HassEntity,
locale: FrontendLocaleData, locale: FrontendLocaleData,
sensorNumericDeviceClasses: string[],
config: HassConfig, config: HassConfig,
entities: HomeAssistant["entities"], entities: HomeAssistant["entities"],
state?: string state?: string
@ -52,6 +35,7 @@ export const computeStateDisplay = (
return computeStateDisplayFromEntityAttributes( return computeStateDisplayFromEntityAttributes(
localize, localize,
locale, locale,
sensorNumericDeviceClasses,
config, config,
entity, entity,
stateObj.entity_id, stateObj.entity_id,
@ -63,6 +47,7 @@ export const computeStateDisplay = (
export const computeStateDisplayFromEntityAttributes = ( export const computeStateDisplayFromEntityAttributes = (
localize: LocalizeFunc, localize: LocalizeFunc,
locale: FrontendLocaleData, locale: FrontendLocaleData,
sensorNumericDeviceClasses: string[],
config: HassConfig, config: HassConfig,
entity: EntityRegistryDisplayEntry | undefined, entity: EntityRegistryDisplayEntry | undefined,
entityId: string, entityId: string,
@ -73,8 +58,15 @@ export const computeStateDisplayFromEntityAttributes = (
return localize(`state.default.${state}`); return localize(`state.default.${state}`);
} }
const domain = computeDomain(entityId);
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
if (isNumericFromAttributes(attributes)) { if (
isNumericFromAttributes(
attributes,
domain === "sensor" ? sensorNumericDeviceClasses : []
)
) {
// state is duration // state is duration
if ( if (
attributes.device_class === "duration" && attributes.device_class === "duration" &&
@ -120,8 +112,6 @@ export const computeStateDisplayFromEntityAttributes = (
return value; return value;
} }
const domain = computeDomain(entityId);
if (domain === "datetime") { if (domain === "datetime") {
const time = new Date(state); const time = new Date(state);
return formatDateTime(time, locale, config); return formatDateTime(time, locale, config);

View File

@ -28,7 +28,15 @@ export const FIXED_DOMAIN_STATES = {
input_button: [], input_button: [],
lawn_mower: ["error", "paused", "mowing", "docked"], lawn_mower: ["error", "paused", "mowing", "docked"],
light: ["on", "off"], light: ["on", "off"],
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"], lock: [
"jammed",
"locked",
"locking",
"unlocked",
"unlocking",
"opening",
"open",
],
media_player: [ media_player: [
"off", "off",
"on", "on",

View File

@ -12,11 +12,10 @@ export const formatLanguageCode = (
} }
}; };
const formatLanguageCodeMem = memoizeOne((locale: FrontendLocaleData) => const formatLanguageCodeMem = memoizeOne(
Intl && "DisplayNames" in Intl (locale: FrontendLocaleData) =>
? new Intl.DisplayNames(locale.language, { new Intl.DisplayNames(locale.language, {
type: "language", type: "language",
fallback: "code", fallback: "code",
}) })
: undefined
); );

View File

@ -11,6 +11,7 @@ declare global {
export interface NavigateOptions { export interface NavigateOptions {
replace?: boolean; replace?: boolean;
data?: any;
} }
export const navigate = (path: string, options?: NavigateOptions) => { export const navigate = (path: string, options?: NavigateOptions) => {
@ -24,7 +25,7 @@ export const navigate = (path: string, options?: NavigateOptions) => {
if (__DEMO__) { if (__DEMO__) {
if (replace) { if (replace) {
mainWindow.history.replaceState( mainWindow.history.replaceState(
mainWindow.history.state?.root ? { root: true } : null, mainWindow.history.state?.root ? { root: true } : options?.data ?? null,
"", "",
`${mainWindow.location.pathname}#${path}` `${mainWindow.location.pathname}#${path}`
); );
@ -33,12 +34,12 @@ export const navigate = (path: string, options?: NavigateOptions) => {
} }
} else if (replace) { } else if (replace) {
mainWindow.history.replaceState( mainWindow.history.replaceState(
mainWindow.history.state?.root ? { root: true } : null, mainWindow.history.state?.root ? { root: true } : options?.data ?? null,
"", "",
path path
); );
} else { } else {
mainWindow.history.pushState(null, "", path); mainWindow.history.pushState(options?.data ?? null, "", path);
} }
fireEvent(mainWindow, "location-changed", { fireEvent(mainWindow, "location-changed", {
replace, replace,

View File

@ -14,8 +14,12 @@ export const isNumericState = (stateObj: HassEntity): boolean =>
isNumericFromAttributes(stateObj.attributes); isNumericFromAttributes(stateObj.attributes);
export const isNumericFromAttributes = ( export const isNumericFromAttributes = (
attributes: HassEntityAttributeBase attributes: HassEntityAttributeBase,
): boolean => !!attributes.unit_of_measurement || !!attributes.state_class; numericDeviceClasses?: string[]
): boolean =>
!!attributes.unit_of_measurement ||
!!attributes.state_class ||
(numericDeviceClasses || []).includes(attributes.device_class || "");
export const numberFormatToLocale = ( export const numberFormatToLocale = (
localeOptions: FrontendLocaleData localeOptions: FrontendLocaleData
@ -59,30 +63,18 @@ export const formatNumber = (
if ( if (
localeOptions?.number_format !== NumberFormat.none && localeOptions?.number_format !== NumberFormat.none &&
!Number.isNaN(Number(num)) && !Number.isNaN(Number(num))
Intl
) { ) {
try { return new Intl.NumberFormat(
return new Intl.NumberFormat( locale,
locale, getDefaultFormatOptions(num, options)
getDefaultFormatOptions(num, options) ).format(Number(num));
).format(Number(num));
} catch (err: any) {
// Don't fail when using "TEST" language
// eslint-disable-next-line no-console
console.error(err);
return new Intl.NumberFormat(
undefined,
getDefaultFormatOptions(num, options)
).format(Number(num));
}
} }
if ( if (
!Number.isNaN(Number(num)) && !Number.isNaN(Number(num)) &&
num !== "" && num !== "" &&
localeOptions?.number_format === NumberFormat.none && localeOptions?.number_format === NumberFormat.none
Intl
) { ) {
// If NumberFormat is none, use en-US format without grouping. // If NumberFormat is none, use en-US format without grouping.
return new Intl.NumberFormat( return new Intl.NumberFormat(

View File

@ -1,5 +1,4 @@
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import "../../resources/intl-polyfill";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData } from "../../data/translation";
export const formatListWithAnds = ( export const formatListWithAnds = (

View File

@ -21,7 +21,8 @@ export const computeFormatFunctions = async (
localize: LocalizeFunc, localize: LocalizeFunc,
locale: FrontendLocaleData, locale: FrontendLocaleData,
config: HassConfig, config: HassConfig,
entities: HomeAssistant["entities"] entities: HomeAssistant["entities"],
sensorNumericDeviceClasses: string[]
): Promise<{ ): Promise<{
formatEntityState: FormatEntityStateFunc; formatEntityState: FormatEntityStateFunc;
formatEntityAttributeValue: FormatEntityAttributeValueFunc; formatEntityAttributeValue: FormatEntityAttributeValueFunc;
@ -35,7 +36,15 @@ export const computeFormatFunctions = async (
return { return {
formatEntityState: (stateObj, state) => formatEntityState: (stateObj, state) =>
computeStateDisplay(localize, stateObj, locale, config, entities, state), computeStateDisplay(
localize,
stateObj,
locale,
sensorNumericDeviceClasses,
config,
entities,
state
),
formatEntityAttributeValue: (stateObj, attribute, value) => formatEntityAttributeValue: (stateObj, attribute, value) =>
computeAttributeValueDisplay( computeAttributeValueDisplay(
localize, localize,

View File

@ -1,6 +1,6 @@
import IntlMessageFormat from "intl-messageformat"; import type { IntlMessageFormat } from "intl-messageformat";
import type { HTMLTemplateResult } from "lit"; import type { HTMLTemplateResult } from "lit";
import { polyfillLocaleData } from "../../resources/locale-data-polyfill"; import { polyfillLocaleData } from "../../resources/polyfills/locale-data-polyfill";
import { Resources, TranslationDict } from "../../types"; import { Resources, TranslationDict } from "../../types";
import { fireEvent } from "../dom/fire_event"; import { fireEvent } from "../dom/fire_event";
@ -89,9 +89,8 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
resources: Resources, resources: Resources,
formats?: FormatsType formats?: FormatsType
): Promise<LocalizeFunc<Keys>> => { ): Promise<LocalizeFunc<Keys>> => {
await import("../../resources/intl-polyfill").then(() => const { IntlMessageFormat } = await import("intl-messageformat");
polyfillLocaleData(language) await polyfillLocaleData(language);
);
// Every time any of the parameters change, invalidate the strings cache. // Every time any of the parameters change, invalidate the strings cache.
cache._localizationCache = {}; cache._localizationCache = {};

View File

@ -313,31 +313,38 @@ export class HaChartBase extends LitElement {
`; `;
} }
private _loading = false;
private async _setupChart() { private async _setupChart() {
if (this._loading) return;
const ctx: CanvasRenderingContext2D = this.renderRoot const ctx: CanvasRenderingContext2D = this.renderRoot
.querySelector("canvas")! .querySelector("canvas")!
.getContext("2d")!; .getContext("2d")!;
this._loading = true;
try {
const ChartConstructor = (await import("../../resources/chartjs")).Chart;
const ChartConstructor = (await import("../../resources/chartjs")).Chart; const computedStyles = getComputedStyle(this);
const computedStyles = getComputedStyle(this); ChartConstructor.defaults.borderColor =
computedStyles.getPropertyValue("--divider-color");
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
"--secondary-text-color"
);
ChartConstructor.defaults.font.family =
computedStyles.getPropertyValue("--mdc-typography-body1-font-family") ||
computedStyles.getPropertyValue("--mdc-typography-font-family") ||
"Roboto, Noto, sans-serif";
ChartConstructor.defaults.borderColor = this.chart = new ChartConstructor(ctx, {
computedStyles.getPropertyValue("--divider-color"); type: this.chartType,
ChartConstructor.defaults.color = computedStyles.getPropertyValue( data: this.data,
"--secondary-text-color" options: this._createOptions(),
); plugins: this._createPlugins(),
ChartConstructor.defaults.font.family = });
computedStyles.getPropertyValue("--mdc-typography-body1-font-family") || } finally {
computedStyles.getPropertyValue("--mdc-typography-font-family") || this._loading = false;
"Roboto, Noto, sans-serif"; }
this.chart = new ChartConstructor(ctx, {
type: this.chartType,
data: this.data,
options: this._createOptions(),
plugins: this._createPlugins(),
});
} }
private _createOptions() { private _createOptions() {

View File

@ -1,4 +1,3 @@
import "element-internals-polyfill";
import { MdAssistChip } from "@material/web/chips/assist-chip"; import { MdAssistChip } from "@material/web/chips/assist-chip";
import { css, html } from "lit"; import { css, html } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";

View File

@ -1,4 +1,3 @@
import "element-internals-polyfill";
import { MdChipSet } from "@material/web/chips/chip-set"; import { MdChipSet } from "@material/web/chips/chip-set";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";

View File

@ -1,4 +1,3 @@
import "element-internals-polyfill";
import { MdFilterChip } from "@material/web/chips/filter-chip"; import { MdFilterChip } from "@material/web/chips/filter-chip";
import { css, html } from "lit"; import { css, html } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";

View File

@ -1,4 +1,3 @@
import "element-internals-polyfill";
import { MdInputChip } from "@material/web/chips/input-chip"; import { MdInputChip } from "@material/web/chips/input-chip";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";

View File

@ -730,6 +730,28 @@ export class HaDataTable extends LitElement {
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
}; };
public expandAllGroups() {
this._collapsedGroups = [];
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
}
public collapseAllGroups() {
if (
!this.groupColumn ||
!this.data.some((item) => item[this.groupColumn!])
) {
return;
}
const grouped = groupBy(this.data, (item) => item[this.groupColumn!]);
if (grouped.undefined) {
// undefined is a reserved group name
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
delete grouped.undefined;
}
this._collapsedGroups = Object.keys(grouped);
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@ -984,6 +1006,7 @@ export class HaDataTable extends LitElement {
padding-top: 12px; padding-top: 12px;
padding-left: 12px; padding-left: 12px;
padding-inline-start: 12px; padding-inline-start: 12px;
padding-inline-end: initial;
width: 100%; width: 100%;
font-weight: 500; font-weight: 500;
display: flex; display: flex;

View File

@ -76,6 +76,8 @@ class HaEntitiesPickerLight extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public entityFilter?: HaEntityPickerEntityFilterFunc; public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Array }) public createDomains?: string[];
protected render() { protected render() {
if (!this.hass) { if (!this.hass) {
return nothing; return nothing;
@ -103,6 +105,7 @@ class HaEntitiesPickerLight extends LitElement {
.value=${entityId} .value=${entityId}
.label=${this.pickedEntityLabel} .label=${this.pickedEntityLabel}
.disabled=${this.disabled} .disabled=${this.disabled}
.createDomains=${this.createDomains}
@value-changed=${this._entityChanged} @value-changed=${this._entityChanged}
></ha-entity-picker> ></ha-entity-picker>
</div> </div>
@ -122,6 +125,7 @@ class HaEntitiesPickerLight extends LitElement {
.label=${this.pickEntityLabel} .label=${this.pickEntityLabel}
.helper=${this.helper} .helper=${this.helper}
.disabled=${this.disabled} .disabled=${this.disabled}
.createDomains=${this.createDomains}
.required=${this.required && !currentEntities.length} .required=${this.required && !currentEntities.length}
@value-changed=${this._addEntity} @value-changed=${this._addEntity}
></ha-entity-picker> ></ha-entity-picker>

View File

@ -405,9 +405,9 @@ export class HaEntityPicker extends LitElement {
this._opened = ev.detail.value; this._opened = ev.detail.value;
} }
private _valueChanged(ev: ValueChangedEvent<string>) { private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
ev.stopPropagation(); ev.stopPropagation();
const newValue = ev.detail.value; const newValue = ev.detail.value?.trim();
if (newValue && newValue.startsWith(CREATE_ID)) { if (newValue && newValue.startsWith(CREATE_ID)) {
const domain = newValue.substring(CREATE_ID.length); const domain = newValue.substring(CREATE_ID.length);
@ -427,13 +427,13 @@ export class HaEntityPicker extends LitElement {
private _filterChanged(ev: CustomEvent): void { private _filterChanged(ev: CustomEvent): void {
const target = ev.target as HaComboBox; const target = ev.target as HaComboBox;
const filterString = ev.detail.value.toLowerCase(); const filterString = ev.detail.value.trim().toLowerCase();
target.filteredItems = filterString.length target.filteredItems = filterString.length
? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states) ? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states)
: this._states; : this._states;
} }
private _setValue(value: string) { private _setValue(value: string | undefined) {
this.value = value; this.value = value;
setTimeout(() => { setTimeout(() => {
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });

View File

@ -1,4 +1,3 @@
import "element-internals-polyfill";
import { MdCircularProgress } from "@material/web/progress/circular-progress"; import { MdCircularProgress } from "@material/web/progress/circular-progress";
import { PropertyValues, css } from "lit"; import { PropertyValues, css } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";

View File

@ -1,14 +1,7 @@
import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { import { customElement, property } from "lit/decorators";
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import "./ha-ripple";
@customElement("ha-control-button") @customElement("ha-control-button")
export class HaControlButton extends LitElement { export class HaControlButton extends LitElement {
@ -16,10 +9,6 @@ export class HaControlButton extends LitElement {
@property() public label?: string; @property() public label?: string;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<button <button
@ -28,54 +17,13 @@ export class HaControlButton extends LitElement {
aria-label=${ifDefined(this.label)} aria-label=${ifDefined(this.label)}
title=${ifDefined(this.label)} title=${ifDefined(this.label)}
.disabled=${Boolean(this.disabled)} .disabled=${Boolean(this.disabled)}
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
> >
<slot></slot> <slot></slot>
${this._shouldRenderRipple && !this.disabled <ha-ripple .disabled=${this.disabled}></ha-ripple>
? html`<mwc-ripple></mwc-ripple>`
: ""}
</button> </button>
`; `;
} }
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
this._rippleHandlers.startPress(evt);
}
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
private handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
private handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
private handleRippleFocus() {
this._rippleHandlers.startFocus();
}
private handleRippleBlur() {
this._rippleHandlers.endFocus();
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
@ -86,6 +34,7 @@ export class HaControlButton extends LitElement {
--control-button-border-radius: 10px; --control-button-border-radius: 10px;
--control-button-padding: 8px; --control-button-padding: 8px;
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
--ha-ripple-color: var(--secondary-text-color);
color: var(--primary-text-color); color: var(--primary-text-color);
width: 40px; width: 40px;
height: 40px; height: 40px;
@ -113,12 +62,14 @@ export class HaControlButton extends LitElement {
outline: none; outline: none;
overflow: hidden; overflow: hidden;
background: none; background: none;
--mdc-ripple-color: var(--control-button-background-color);
/* For safari border-radius overflow */ /* For safari border-radius overflow */
z-index: 0; z-index: 0;
font-size: inherit; font-size: inherit;
color: inherit; color: inherit;
} }
.button:focus-visible {
--control-button-background-opacity: 0.4;
}
.button::before { .button::before {
content: ""; content: "";
position: absolute; position: absolute;

View File

@ -1,22 +1,14 @@
import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { SelectBase } from "@material/mwc-select/mwc-select-base"; import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { mdiMenuDown } from "@mdi/js"; import { mdiMenuDown } from "@mdi/js";
import { css, html, nothing } from "lit"; import { css, html, nothing } from "lit";
import { import { customElement, property, query } from "lit/decorators";
customElement,
eventOptions,
property,
query,
queryAsync,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { 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"; import "./ha-icon";
import type { HaIcon } from "./ha-icon"; import type { HaIcon } from "./ha-icon";
import "./ha-ripple";
import "./ha-svg-icon"; import "./ha-svg-icon";
import type { HaSvgIcon } from "./ha-svg-icon"; import type { HaSvgIcon } from "./ha-svg-icon";
@ -32,10 +24,6 @@ export class HaControlSelectMenu extends SelectBase {
@property({ type: Boolean, attribute: "hide-label" }) @property({ type: Boolean, attribute: "hide-label" })
public hideLabel = false; public hideLabel = false;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
public override render() { public override render() {
const classes = { const classes = {
"select-disabled": this.disabled, "select-disabled": this.disabled,
@ -69,17 +57,10 @@ export class HaControlSelectMenu extends SelectBase {
aria-labelledby=${ifDefined(labelledby)} aria-labelledby=${ifDefined(labelledby)}
aria-label=${ifDefined(labelAttribute)} aria-label=${ifDefined(labelAttribute)}
aria-required=${this.required} aria-required=${this.required}
@click=${this.onClick}
@focus=${this.onFocus} @focus=${this.onFocus}
@blur=${this.onBlur} @blur=${this.onBlur}
@click=${this.onClick}
@keydown=${this.onKeydown} @keydown=${this.onKeydown}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
> >
${this.renderIcon()} ${this.renderIcon()}
<div class="content"> <div class="content">
@ -91,9 +72,7 @@ export class HaControlSelectMenu extends SelectBase {
: nothing} : nothing}
</div> </div>
${this.renderArrow()} ${this.renderArrow()}
${this._shouldRenderRipple && !this.disabled <ha-ripple .disabled=${this.disabled}></ha-ripple>
? html` <mwc-ripple></mwc-ripple> `
: nothing}
</div> </div>
${this.renderMenu()} ${this.renderMenu()}
</div> </div>
@ -135,46 +114,6 @@ export class HaControlSelectMenu extends SelectBase {
`; `;
} }
protected onFocus() {
this.handleRippleFocus();
super.onFocus();
}
protected onBlur() {
this.handleRippleBlur();
super.onBlur();
}
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
this._rippleHandlers.startPress(evt);
}
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
private handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
private handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
private handleRippleFocus() {
this._rippleHandlers.startFocus();
}
private handleRippleBlur() {
this._rippleHandlers.endFocus();
}
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated); window.addEventListener("translations-updated", this._translationsUpdated);
@ -204,6 +143,7 @@ export class HaControlSelectMenu extends SelectBase {
--control-select-menu-height: 48px; --control-select-menu-height: 48px;
--control-select-menu-padding: 6px 10px; --control-select-menu-padding: 6px 10px;
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
--ha-ripple-color: var(--secondary-text-color);
font-size: 14px; font-size: 14px;
line-height: 1.4; line-height: 1.4;
width: auto; width: auto;
@ -224,7 +164,6 @@ export class HaControlSelectMenu extends SelectBase {
outline: none; outline: none;
overflow: hidden; overflow: hidden;
background: none; background: none;
--mdc-ripple-color: var(--control-select-menu-background-color);
/* For safari border-radius overflow */ /* For safari border-radius overflow */
z-index: 0; z-index: 0;
transition: color 180ms ease-in-out; transition: color 180ms ease-in-out;
@ -264,6 +203,10 @@ export class HaControlSelectMenu extends SelectBase {
letter-spacing: inherit; letter-spacing: inherit;
} }
.select-anchor:focus-visible {
--control-select-menu-background-opacity: 0.4;
}
.select-anchor::before { .select-anchor::before {
content: ""; content: "";
position: absolute; position: absolute;

View File

@ -4,7 +4,6 @@ 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 { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
import "../resources/intl-polyfill";
import "./ha-list-item"; import "./ha-list-item";
import "./ha-select"; import "./ha-select";
import type { HaSelect } from "./ha-select"; import type { HaSelect } from "./ha-select";
@ -282,14 +281,10 @@ export class HaCountryPicker extends LitElement {
private _getOptions = memoizeOne( private _getOptions = memoizeOne(
(language?: string, countries?: string[]) => { (language?: string, countries?: string[]) => {
let options: { label: string; value: string }[] = []; let options: { label: string; value: string }[] = [];
const countryDisplayNames = const countryDisplayNames = new Intl.DisplayNames(language, {
Intl && "DisplayNames" in Intl type: "region",
? new Intl.DisplayNames(language, { fallback: "code",
type: "region", });
fallback: "code",
})
: undefined;
if (countries) { if (countries) {
options = countries.map((country) => ({ options = countries.map((country) => ({
value: country, value: country,

View File

@ -4,7 +4,6 @@ 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 { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
import "../resources/intl-polyfill";
import "./ha-list-item"; import "./ha-list-item";
import "./ha-select"; import "./ha-select";
import type { HaSelect } from "./ha-select"; import type { HaSelect } from "./ha-select";
@ -170,12 +169,9 @@ const CURRENCIES = [
]; ];
const curSymbol = (currency: string, locale?: string) => const curSymbol = (currency: string, locale?: string) =>
Intl && "NumberFormat" in Intl new Intl.NumberFormat(locale, { style: "currency", currency })
? new Intl.NumberFormat(locale, { style: "currency", currency }) .formatToParts(1)
.formatToParts(1) .find((x) => x.type === "currency")?.value;
.find((x) => x.type === "currency")?.value
: currency;
@customElement("ha-currency-picker") @customElement("ha-currency-picker")
export class HaCurrencyPicker extends LitElement { export class HaCurrencyPicker extends LitElement {
@property() public language = "en"; @property() public language = "en";
@ -189,13 +185,10 @@ export class HaCurrencyPicker extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false; @property({ type: Boolean, reflect: true }) public disabled = false;
private _getOptions = memoizeOne((language?: string) => { private _getOptions = memoizeOne((language?: string) => {
const currencyDisplayNames = const currencyDisplayNames = new Intl.DisplayNames(language, {
Intl && "DisplayNames" in Intl type: "currency",
? new Intl.DisplayNames(language, { fallback: "code",
type: "currency", });
fallback: "code",
})
: undefined;
const options = CURRENCIES.map((currency) => ({ const options = CURRENCIES.map((currency) => ({
value: currency, value: currency,
label: `${ label: `${

View File

@ -21,6 +21,8 @@ export class HaExpansionPanel extends LitElement {
@property({ type: Boolean, reflect: true }) leftChevron = false; @property({ type: Boolean, reflect: true }) leftChevron = false;
@property({ type: Boolean, reflect: true }) noCollapse = false;
@property() header?: string; @property() header?: string;
@property() secondary?: string; @property() secondary?: string;
@ -34,16 +36,17 @@ export class HaExpansionPanel extends LitElement {
<div class="top ${classMap({ expanded: this.expanded })}"> <div class="top ${classMap({ expanded: this.expanded })}">
<div <div
id="summary" id="summary"
class=${classMap({ noCollapse: this.noCollapse })}
@click=${this._toggleContainer} @click=${this._toggleContainer}
@keydown=${this._toggleContainer} @keydown=${this._toggleContainer}
@focus=${this._focusChanged} @focus=${this._focusChanged}
@blur=${this._focusChanged} @blur=${this._focusChanged}
role="button" role="button"
tabindex="0" tabindex=${this.noCollapse ? -1 : 0}
aria-expanded=${this.expanded} aria-expanded=${this.expanded}
aria-controls="sect1" aria-controls="sect1"
> >
${this.leftChevron ${this.leftChevron && !this.noCollapse
? html` ? html`
<ha-svg-icon <ha-svg-icon
.path=${mdiChevronDown} .path=${mdiChevronDown}
@ -57,7 +60,7 @@ export class HaExpansionPanel extends LitElement {
<slot class="secondary" name="secondary">${this.secondary}</slot> <slot class="secondary" name="secondary">${this.secondary}</slot>
</div> </div>
</slot> </slot>
${!this.leftChevron ${!this.leftChevron && !this.noCollapse
? html` ? html`
<ha-svg-icon <ha-svg-icon
.path=${mdiChevronDown} .path=${mdiChevronDown}
@ -106,6 +109,9 @@ export class HaExpansionPanel extends LitElement {
return; return;
} }
ev.preventDefault(); ev.preventDefault();
if (this.noCollapse) {
return;
}
const newExpanded = !this.expanded; const newExpanded = !this.expanded;
fireEvent(this, "expanded-will-change", { expanded: newExpanded }); fireEvent(this, "expanded-will-change", { expanded: newExpanded });
this._container.style.overflow = "hidden"; this._container.style.overflow = "hidden";
@ -130,6 +136,9 @@ export class HaExpansionPanel extends LitElement {
} }
private _focusChanged(ev) { private _focusChanged(ev) {
if (this.noCollapse) {
return;
}
this.shadowRoot!.querySelector(".top")!.classList.toggle( this.shadowRoot!.querySelector(".top")!.classList.toggle(
"focused", "focused",
ev.type === "focus" ev.type === "focus"
@ -191,6 +200,9 @@ export class HaExpansionPanel extends LitElement {
font-weight: 500; font-weight: 500;
outline: none; outline: none;
} }
#summary.noCollapse {
cursor: default;
}
.summary-icon.expanded { .summary-icon.expanded {
transform: rotate(180deg); transform: rotate(180deg);

View File

@ -1,7 +1,14 @@
import { SelectedDetail } from "@material/mwc-list"; import { SelectedDetail } from "@material/mwc-list";
import "@material/mwc-menu/mwc-menu-surface"; import "@material/mwc-menu/mwc-menu-surface";
import { mdiFilterVariantRemove } from "@mdi/js"; import { mdiFilterVariantRemove } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { Blueprints, fetchBlueprints } from "../data/blueprint"; import { Blueprints, fetchBlueprints } from "../data/blueprint";
@ -25,6 +32,16 @@ export class HaFilterBlueprints extends LitElement {
@state() private _blueprints?: Blueprints; @state() private _blueprints?: Blueprints;
public willUpdate(properties: PropertyValues) {
super.willUpdate(properties);
if (!this.hasUpdated) {
if (this.value?.length) {
this._findRelated();
}
}
}
protected render() { protected render() {
return html` return html`
<ha-expansion-panel <ha-expansion-panel
@ -96,7 +113,6 @@ export class HaFilterBlueprints extends LitElement {
ev: CustomEvent<SelectedDetail<Set<number>>> ev: CustomEvent<SelectedDetail<Set<number>>>
) { ) {
const blueprints = this._blueprints!; const blueprints = this._blueprints!;
const relatedPromises: Promise<RelatedResult>[] = [];
if (!ev.detail.index.size) { if (!ev.detail.index.size) {
fireEvent(this, "data-table-filter-changed", { fireEvent(this, "data-table-filter-changed", {
@ -112,13 +128,33 @@ export class HaFilterBlueprints extends LitElement {
for (const index of ev.detail.index) { for (const index of ev.detail.index) {
const blueprintId = Object.keys(blueprints)[index]; const blueprintId = Object.keys(blueprints)[index];
value.push(blueprintId); value.push(blueprintId);
}
this.value = value;
this._findRelated();
}
private async _findRelated() {
if (!this.value?.length) {
fireEvent(this, "data-table-filter-changed", {
value: [],
items: undefined,
});
this.value = [];
return;
}
const relatedPromises: Promise<RelatedResult>[] = [];
for (const blueprintId of this.value) {
if (this.type) { if (this.type) {
relatedPromises.push( relatedPromises.push(
findRelated(this.hass, `${this.type}_blueprint`, blueprintId) findRelated(this.hass, `${this.type}_blueprint`, blueprintId)
); );
} }
} }
this.value = value;
const results = await Promise.all(relatedPromises); const results = await Promise.all(relatedPromises);
const items: Set<string> = new Set(); const items: Set<string> = new Set();
for (const result of results) { for (const result of results) {
@ -128,7 +164,7 @@ export class HaFilterBlueprints extends LitElement {
} }
fireEvent(this, "data-table-filter-changed", { fireEvent(this, "data-table-filter-changed", {
value, value: this.value,
items: this.type ? items : undefined, items: this.type ? items : undefined,
}); });
} }

View File

@ -41,6 +41,9 @@ export class HaFilterDevices extends LitElement {
if (!this.hasUpdated) { if (!this.hasUpdated) {
loadVirtualizer(); loadVirtualizer();
if (this.value?.length) {
this._findRelated();
}
} }
} }

View File

@ -87,8 +87,14 @@ export class HaFilterDomains extends LitElement {
Object.keys(states).forEach((entityId) => { Object.keys(states).forEach((entityId) => {
domains.add(computeDomain(entityId)); domains.add(computeDomain(entityId));
}); });
return Array.from(domains)
.filter((domain) => !filter || domain.toLowerCase().includes(filter)) return Array.from(domains.values())
.filter(
(entry) =>
!filter ||
entry.toLowerCase().includes(filter) ||
domainToName(this.hass.localize, entry).toLowerCase().includes(filter)
)
.sort((a, b) => stringCompare(a, b, this.hass.locale.language)); .sort((a, b) => stringCompare(a, b, this.hass.locale.language));
}); });
@ -163,14 +169,14 @@ export class HaFilterDomains extends LitElement {
align-items: center; align-items: center;
} }
.header ha-icon-button { .header ha-icon-button {
margin-inline-start: auto; margin-inline-start: initial;
margin-inline-end: 8px; margin-inline-end: 8px;
} }
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;
margin-inline-start: 8px; margin-inline-start: 8px;
margin-inline-end: 0; margin-inline-end: initial;
min-width: 16px; min-width: 16px;
box-sizing: border-box; box-sizing: border-box;
border-radius: 50%; border-radius: 50%;

View File

@ -42,6 +42,9 @@ export class HaFilterEntities extends LitElement {
if (!this.hasUpdated) { if (!this.hasUpdated) {
loadVirtualizer(); loadVirtualizer();
if (this.value?.length) {
this._findRelated();
}
} }
} }
@ -186,15 +189,12 @@ export class HaFilterEntities extends LitElement {
return; return;
} }
const value: string[] = [];
for (const entityId of this.value) { for (const entityId of this.value) {
value.push(entityId);
if (this.type) { if (this.type) {
relatedPromises.push(findRelated(this.hass, "entity", entityId)); relatedPromises.push(findRelated(this.hass, "entity", entityId));
} }
} }
this.value = value;
const results = await Promise.all(relatedPromises); const results = await Promise.all(relatedPromises);
const items: Set<string> = new Set(); const items: Set<string> = new Set();
for (const result of results) { for (const result of results) {
@ -204,7 +204,7 @@ export class HaFilterEntities extends LitElement {
} }
fireEvent(this, "data-table-filter-changed", { fireEvent(this, "data-table-filter-changed", {
value, value: this.value,
items: this.type ? items : undefined, items: this.type ? items : undefined,
}); });
} }

View File

@ -1,7 +1,14 @@
import "@material/mwc-menu/mwc-menu-surface"; import "@material/mwc-menu/mwc-menu-surface";
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js"; import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
@ -42,6 +49,16 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
@state() private _floors?: FloorRegistryEntry[]; @state() private _floors?: FloorRegistryEntry[];
public willUpdate(properties: PropertyValues) {
super.willUpdate(properties);
if (!this.hasUpdated) {
if (this.value?.floors?.length || this.value?.areas?.length) {
this._findRelated();
}
}
}
protected render() { protected render() {
const areas = this._areas(this.hass.areas, this._floors); const areas = this._areas(this.hass.areas, this._floors);
@ -190,6 +207,10 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
} }
} }
protected firstUpdated() {
this._findRelated();
}
private _expandedWillChange(ev) { private _expandedWillChange(ev) {
this._shouldRender = ev.detail.expanded; this._shouldRender = ev.detail.expanded;
} }

View File

@ -1,4 +1,3 @@
import "@material/mwc-list/mwc-list-item";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { import {
ComboBoxDataProviderCallback, ComboBoxDataProviderCallback,
@ -11,6 +10,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { customIcons } from "../data/custom_icons"; import { customIcons } from "../data/custom_icons";
import { HomeAssistant, ValueChangedEvent } from "../types"; import { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-combo-box"; import "./ha-combo-box";
import "./ha-list-item";
import "./ha-icon"; import "./ha-icon";
type IconItem = { type IconItem = {
@ -67,10 +67,10 @@ const loadCustomIconItems = async (iconsetPrefix: string) => {
}; };
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) => const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) =>
html`<mwc-list-item graphic="avatar"> html`<ha-list-item graphic="avatar">
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon> <ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
${item.icon} ${item.icon}
</mwc-list-item>`; </ha-list-item>`;
@customElement("ha-icon-picker") @customElement("ha-icon-picker")
export class HaIconPicker extends LitElement { export class HaIconPicker extends LitElement {

View File

@ -1,6 +1,5 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "@material/web/ripple/ripple";
@customElement("ha-label") @customElement("ha-label")
class HaLabel extends LitElement { class HaLabel extends LitElement {
@ -11,7 +10,6 @@ class HaLabel extends LitElement {
<span class="content"> <span class="content">
<slot name="icon"></slot> <slot name="icon"></slot>
<slot></slot> <slot></slot>
<md-ripple></md-ripple>
</span> </span>
`; `;
} }
@ -27,7 +25,6 @@ class HaLabel extends LitElement {
0.15 0.15
); );
--ha-label-background-opacity: 1; --ha-label-background-opacity: 1;
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
display: inline-flex; display: inline-flex;

View File

@ -6,7 +6,6 @@ import { stopPropagation } from "../common/dom/stop_propagation";
import { formatLanguageCode } from "../common/language/format_language"; import { formatLanguageCode } from "../common/language/format_language";
import { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
import { FrontendLocaleData } from "../data/translation"; import { FrontendLocaleData } from "../data/translation";
import "../resources/intl-polyfill";
import { translationMetadata } from "../resources/translations-metadata"; import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-list-item"; import "./ha-list-item";

View File

@ -1,7 +1,6 @@
import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { MdListItem } from "@material/web/list/list-item"; import { MdListItem } from "@material/web/list/list-item";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-list-item-new") @customElement("ha-list-item-new")
export class HaListItemNew extends MdListItem { export class HaListItemNew extends MdListItem {

View File

@ -100,6 +100,7 @@ export class HaListItem extends ListItemBase {
span.material-icons:first-of-type, span.material-icons:first-of-type,
span.material-icons:last-of-type { span.material-icons:last-of-type {
direction: rtl !important; direction: rtl !important;
--direction: rtl;
} }
` `
: css``, : css``,

View File

@ -1,7 +1,6 @@
import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { MdList } from "@material/web/list/list"; import { MdList } from "@material/web/list/list";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-list-new") @customElement("ha-list-new")
export class HaListNew extends MdList { export class HaListNew extends MdList {

View File

@ -1,5 +1,4 @@
import { MdMenuItem } from "@material/web/menu/menu-item"; import { MdMenuItem } from "@material/web/menu/menu-item";
import "element-internals-polyfill";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";

View File

@ -1,7 +1,6 @@
import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { css } from "lit";
import { MdMenu } from "@material/web/menu/menu"; import { MdMenu } from "@material/web/menu/menu";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-menu") @customElement("ha-menu")
export class HaMenu extends MdMenu { export class HaMenu extends MdMenu {

View File

@ -1,7 +1,6 @@
import { MdOutlinedButton } from "@material/web/button/outlined-button";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { MdOutlinedButton } from "@material/web/button/outlined-button";
@customElement("ha-outlined-button") @customElement("ha-outlined-button")
export class HaOutlinedButton extends MdOutlinedButton { export class HaOutlinedButton extends MdOutlinedButton {

View File

@ -1,5 +1,4 @@
import { MdOutlinedField } from "@material/web/field/outlined-field"; import { MdOutlinedField } from "@material/web/field/outlined-field";
import "element-internals-polyfill";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { literal } from "lit/static-html"; import { literal } from "lit/static-html";

View File

@ -1,7 +1,6 @@
import { MdOutlinedIconButton } from "@material/web/iconbutton/outlined-icon-button";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { MdOutlinedIconButton } from "@material/web/iconbutton/outlined-icon-button";
@customElement("ha-outlined-icon-button") @customElement("ha-outlined-icon-button")
export class HaOutlinedIconButton extends MdOutlinedIconButton { export class HaOutlinedIconButton extends MdOutlinedIconButton {

View File

@ -1,5 +1,4 @@
import { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field"; import { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field";
import "element-internals-polyfill";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { literal } from "lit/static-html"; import { literal } from "lit/static-html";

View File

@ -2,6 +2,7 @@ import { mdiImagePlus } from "@mdi/js";
import { LitElement, TemplateResult, css, html } from "lit"; import { LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { haStyle } from "../resources/styles";
import { createImage, generateImageThumbnailUrl } from "../data/image_upload"; import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { import {
@ -31,6 +32,8 @@ export class HaPictureUpload extends LitElement {
@property({ attribute: false }) public cropOptions?: CropOptions; @property({ attribute: false }) public cropOptions?: CropOptions;
@property({ type: Boolean }) public original = false;
@property({ type: Number }) public size = 512; @property({ type: Number }) public size = 512;
@state() private _uploading = false; @state() private _uploading = false;
@ -60,13 +63,15 @@ export class HaPictureUpload extends LitElement {
alt=${this.currentImageAltText || alt=${this.currentImageAltText ||
this.hass.localize("ui.components.picture-upload.current_image_alt")} this.hass.localize("ui.components.picture-upload.current_image_alt")}
/> />
<ha-button <div>
@click=${this._handleChangeClick} <ha-button
.label=${this.hass.localize( @click=${this._handleChangeClick}
"ui.components.picture-upload.change_picture" .label=${this.hass.localize(
)} "ui.components.picture-upload.change_picture"
> )}
</ha-button> >
</ha-button>
</div>
</div> </div>
</div>`; </div>`;
} }
@ -122,7 +127,11 @@ export class HaPictureUpload extends LitElement {
this._uploading = true; this._uploading = true;
try { try {
const media = await createImage(this.hass, file); const media = await createImage(this.hass, file);
this.value = generateImageThumbnailUrl(media.id, this.size); this.value = generateImageThumbnailUrl(
media.id,
this.size,
this.original
);
fireEvent(this, "change"); fireEvent(this, "change");
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {
@ -134,32 +143,35 @@ export class HaPictureUpload extends LitElement {
} }
static get styles() { static get styles() {
return css` return [
:host { haStyle,
display: block; css`
height: 240px; :host {
} display: block;
ha-file-upload { height: 240px;
height: 100%; }
} ha-file-upload {
.center-vertical { height: 100%;
display: flex; }
align-items: center; .center-vertical {
height: 100%; display: flex;
} align-items: center;
.value { height: 100%;
width: 100%; }
display: flex; .value {
flex-direction: column; width: 100%;
align-items: center; display: flex;
} flex-direction: column;
img { align-items: center;
max-width: 100%; }
max-height: 200px; img {
margin-bottom: 4px; max-width: 100%;
border-radius: var(--file-upload-image-border-radius); max-height: 200px;
} margin-bottom: 4px;
`; border-radius: var(--file-upload-image-border-radius);
}
`,
];
} }
} }

View File

@ -0,0 +1,62 @@
import { AttachableController } from "@material/web/internal/controller/attachable-controller";
import { MdRipple } from "@material/web/ripple/ripple";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-ripple")
export class HaRipple extends MdRipple {
private readonly attachableTouchController = new AttachableController(
this,
this.onTouchControlChange.bind(this)
);
attach(control: HTMLElement) {
super.attach(control);
this.attachableTouchController.attach(control);
}
detach() {
super.detach();
this.attachableTouchController.detach();
}
private _handleTouchEnd = () => {
if (!this.disabled) {
// @ts-ignore
super.endPressAnimation();
}
};
private onTouchControlChange(
prev: HTMLElement | null,
next: HTMLElement | null
) {
// Add touchend event to clean ripple on touch devices using action handler
prev?.removeEventListener("touchend", this._handleTouchEnd);
next?.addEventListener("touchend", this._handleTouchEnd);
}
static override styles = [
...super.styles,
css`
:host {
--md-ripple-hover-opacity: var(--ha-ripple-hover-opacity, 0.08);
--md-ripple-pressed-opacity: var(--ha-ripple-pressed-opacity, 0.12);
--md-ripple-hover-color: var(
--ha-ripple-hover-color,
var(--ha-ripple-color, var(--secondary-text-color))
);
--md-ripple-pressed-color: var(
--ha-ripple-pressed-color,
var(--ha-ripple-color, var(--secondary-text-color))
);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-ripple": HaRipple;
}
}

View File

@ -8,7 +8,10 @@ import {
fetchEntitySourcesWithCache, fetchEntitySourcesWithCache,
} from "../../data/entity_sources"; } from "../../data/entity_sources";
import type { EntitySelector } from "../../data/selector"; import type { EntitySelector } from "../../data/selector";
import { filterSelectorEntities } from "../../data/selector"; import {
filterSelectorEntities,
computeCreateDomains,
} from "../../data/selector";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../entity/ha-entities-picker"; import "../entity/ha-entities-picker";
import "../entity/ha-entity-picker"; import "../entity/ha-entity-picker";
@ -31,6 +34,8 @@ export class HaEntitySelector extends LitElement {
@property({ type: Boolean }) public required = true; @property({ type: Boolean }) public required = true;
@state() private _createDomains: string[] | undefined;
private _hasIntegration(selector: EntitySelector) { private _hasIntegration(selector: EntitySelector) {
return ( return (
selector.entity?.filter && selector.entity?.filter &&
@ -64,6 +69,7 @@ export class HaEntitySelector extends LitElement {
.includeEntities=${this.selector.entity?.include_entities} .includeEntities=${this.selector.entity?.include_entities}
.excludeEntities=${this.selector.entity?.exclude_entities} .excludeEntities=${this.selector.entity?.exclude_entities}
.entityFilter=${this._filterEntities} .entityFilter=${this._filterEntities}
.createDomains=${this._createDomains}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
allow-custom-entity allow-custom-entity
@ -79,6 +85,7 @@ export class HaEntitySelector extends LitElement {
.includeEntities=${this.selector.entity.include_entities} .includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities} .excludeEntities=${this.selector.entity.exclude_entities}
.entityFilter=${this._filterEntities} .entityFilter=${this._filterEntities}
.createDomains=${this._createDomains}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
></ha-entities-picker> ></ha-entities-picker>
@ -96,6 +103,9 @@ export class HaEntitySelector extends LitElement {
this._entitySources = sources; this._entitySources = sources;
}); });
} }
if (changedProps.has("selector")) {
this._createDomains = computeCreateDomains(this.selector);
}
} }
private _filterEntities = (entity: HassEntity): boolean => { private _filterEntities = (entity: HassEntity): boolean => {

View File

@ -0,0 +1,145 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { ImageSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-textarea";
import "../ha-textfield";
import "../ha-picture-upload";
import "../ha-radio";
import type { HaPictureUpload } from "../ha-picture-upload";
import { URL_PREFIX } from "../../data/image_upload";
@customElement("ha-selector-image")
export class HaImageSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: any;
@property() public name?: string;
@property() public label?: string;
@property() public placeholder?: string;
@property() public helper?: string;
@property({ attribute: false }) public selector!: ImageSelector;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@state() private showUpload = false;
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
if (!this.value || this.value.startsWith(URL_PREFIX)) {
this.showUpload = true;
}
}
protected render() {
return html`
<div>
<label>
${this.hass.localize("ui.components.selectors.image.select_image")}
<ha-formfield
.label=${this.hass.localize("ui.components.selectors.image.upload")}
>
<ha-radio
name="mode"
value="upload"
.checked=${this.showUpload}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize("ui.components.selectors.image.url")}
>
<ha-radio
name="mode"
value="url"
.checked=${!this.showUpload}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
</label>
${!this.showUpload
? html`
<ha-textfield
.name=${this.name}
.value=${this.value || ""}
.placeholder=${this.placeholder || ""}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
@input=${this._handleChange}
.label=${this.label || ""}
.required=${this.required}
></ha-textfield>
`
: html`
<ha-picture-upload
.hass=${this.hass}
.value=${this.value?.startsWith(URL_PREFIX) ? this.value : null}
.original=${this.selector.image?.original}
.cropOptions=${this.selector.image?.crop}
@change=${this._pictureChanged}
></ha-picture-upload>
`}
</div>
`;
}
private _radioGroupPicked(ev): void {
this.showUpload = ev.target.value === "upload";
}
private _pictureChanged(ev) {
const value = (ev.target as HaPictureUpload).value;
fireEvent(this, "value-changed", { value: value ?? undefined });
}
private _handleChange(ev) {
let value = ev.target.value;
if (this.value === value) {
return;
}
if (value === "" && !this.required) {
value = undefined;
}
fireEvent(this, "value-changed", { value });
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
position: relative;
}
div {
display: flex;
flex-direction: column;
}
label {
display: flex;
flex-direction: column;
}
ha-textarea,
ha-textfield {
width: 100%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-image": HaImageSelector;
}
}

View File

@ -278,6 +278,14 @@ export class HaSelectSelector extends LitElement {
private _valueChanged(ev) { private _valueChanged(ev) {
ev.stopPropagation(); ev.stopPropagation();
if (ev.detail?.index === -1 && this.value !== undefined) {
fireEvent(this, "value-changed", {
value: undefined,
});
return;
}
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;

View File

@ -22,6 +22,7 @@ import {
filterSelectorDevices, filterSelectorDevices,
filterSelectorEntities, filterSelectorEntities,
TargetSelector, TargetSelector,
computeCreateDomains,
} from "../../data/selector"; } from "../../data/selector";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-target-picker"; import "../ha-target-picker";
@ -42,6 +43,8 @@ export class HaTargetSelector extends LitElement {
@state() private _entitySources?: EntitySources; @state() private _entitySources?: EntitySources;
@state() private _createDomains: string[] | undefined;
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
private _hasIntegration(selector: TargetSelector) { private _hasIntegration(selector: TargetSelector) {
@ -68,6 +71,9 @@ export class HaTargetSelector extends LitElement {
this._entitySources = sources; this._entitySources = sources;
}); });
} }
if (changedProperties.has("selector")) {
this._createDomains = computeCreateDomains(this.selector);
}
} }
protected render() { protected render() {
@ -82,7 +88,7 @@ export class HaTargetSelector extends LitElement {
.deviceFilter=${this._filterDevices} .deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities} .entityFilter=${this._filterEntities}
.disabled=${this.disabled} .disabled=${this.disabled}
.createDomains=${this.selector.target?.create_domains} .createDomains=${this._createDomains}
></ha-target-picker>`; ></ha-target-picker>`;
} }

View File

@ -32,6 +32,7 @@ const LOAD_ELEMENTS = {
file: () => import("./ha-selector-file"), file: () => import("./ha-selector-file"),
floor: () => import("./ha-selector-floor"), floor: () => import("./ha-selector-floor"),
label: () => import("./ha-selector-label"), label: () => import("./ha-selector-label"),
image: () => import("./ha-selector-image"),
language: () => import("./ha-selector-language"), language: () => import("./ha-selector-language"),
navigation: () => import("./ha-selector-navigation"), navigation: () => import("./ha-selector-navigation"),
number: () => import("./ha-selector-number"), number: () => import("./ha-selector-number"),

View File

@ -44,7 +44,6 @@ import "./ha-service-picker";
import "./ha-settings-row"; import "./ha-settings-row";
import "./ha-yaml-editor"; import "./ha-yaml-editor";
import type { HaYamlEditor } from "./ha-yaml-editor"; import type { HaYamlEditor } from "./ha-yaml-editor";
import { isHelperDomain } from "../panels/config/helpers/const";
const attributeFilter = (values: any[], attribute: any) => { const attributeFilter = (values: any[], attribute: any) => {
if (typeof attribute === "object") { if (typeof attribute === "object") {
@ -366,12 +365,8 @@ export class HaServiceControl extends LitElement {
} }
private _targetSelector = memoizeOne( private _targetSelector = memoizeOne(
(targetSelector: TargetSelector | null | undefined, domain?: string) => { (targetSelector: TargetSelector | null | undefined) =>
const create_domains = isHelperDomain(domain) ? [domain] : undefined; targetSelector ? { target: { ...targetSelector } } : { target: {} }
return targetSelector
? { target: { ...targetSelector, create_domains } }
: { target: { create_domains } };
}
); );
protected render() { protected render() {
@ -462,8 +457,7 @@ export class HaServiceControl extends LitElement {
><ha-selector ><ha-selector
.hass=${this.hass} .hass=${this.hass}
.selector=${this._targetSelector( .selector=${this._targetSelector(
serviceData.target as TargetSelector, serviceData.target as TargetSelector
domain
)} )}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._targetChanged} @value-changed=${this._targetChanged}

View File

@ -1,7 +1,6 @@
import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { MdSlider } from "@material/web/slider/slider"; import { MdSlider } from "@material/web/slider/slider";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators";
import { mainWindow } from "../common/dom/get_main_window"; import { mainWindow } from "../common/dom/get_main_window";
@customElement("ha-slider") @customElement("ha-slider")

View File

@ -1,7 +1,6 @@
import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { css } from "lit";
import { MdSubMenu } from "@material/web/menu/sub-menu"; import { MdSubMenu } from "@material/web/menu/sub-menu";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-sub-menu") @customElement("ha-sub-menu")
export class HaSubMenu extends MdSubMenu { export class HaSubMenu extends MdSubMenu {

View File

@ -1,15 +1,7 @@
import type { Ripple } from "@material/mwc-ripple";
import "@material/mwc-ripple/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { import { customElement, property } from "lit/decorators";
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import "./ha-ripple";
@customElement("ha-tab") @customElement("ha-tab")
export class HaTab extends LitElement { export class HaTab extends LitElement {
@ -19,10 +11,6 @@ export class HaTab extends LitElement {
@property() public name?: string; @property() public name?: string;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div <div
@ -30,60 +18,21 @@ export class HaTab extends LitElement {
role="tab" role="tab"
aria-selected=${this.active} aria-selected=${this.active}
aria-label=${ifDefined(this.name)} aria-label=${ifDefined(this.name)}
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
@keydown=${this._handleKeyDown} @keydown=${this._handleKeyDown}
> >
${this.narrow ? html`<slot name="icon"></slot>` : ""} ${this.narrow ? html`<slot name="icon"></slot>` : ""}
<span class="name">${this.name}</span> <span class="name">${this.name}</span>
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""} <ha-ripple></ha-ripple>
</div> </div>
`; `;
} }
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
private _handleKeyDown(ev: KeyboardEvent): void { private _handleKeyDown(ev: KeyboardEvent): void {
if (ev.key === "Enter") { if (ev.key === "Enter") {
(ev.target as HTMLElement).click(); (ev.target as HTMLElement).click();
} }
} }
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
this._rippleHandlers.startPress(evt);
}
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
private handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
private handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
private handleRippleFocus() {
this._rippleHandlers.startFocus();
}
private handleRippleBlur() {
this._rippleHandlers.endFocus();
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
div { div {
@ -126,6 +75,15 @@ export class HaTab extends LitElement {
:host([narrow]) div { :host([narrow]) div {
padding: 0 4px; padding: 0 4px;
} }
div:focus-visible:before {
position: absolute;
display: block;
content: "";
inset: 0;
background-color: var(--secondary-text-color);
opacity: 0.08;
}
`; `;
} }
} }

View File

@ -206,6 +206,7 @@ export class HaTextField extends TextFieldBase {
.mdc-floating-label, .mdc-floating-label,
.mdc-text-field__input[type="number"] { .mdc-text-field__input[type="number"] {
direction: rtl; direction: rtl;
--direction: rtl;
} }
` `
: css``, : css``,

View File

@ -22,7 +22,6 @@ import {
} from "../../common/dom/setup-leaflet-map"; } from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { loadPolyfillIfNeeded } from "../../resources/resize-observer.polyfill";
import { HomeAssistant, ThemeMode } from "../../types"; import { HomeAssistant, ThemeMode } from "../../types";
import { isTouch } from "../../util/is_touch"; import { isTouch } from "../../util/is_touch";
import "../ha-icon-button"; import "../ha-icon-button";
@ -178,16 +177,24 @@ export class HaMap extends ReactiveElement {
map!.classList.toggle("forced-light", this.themeMode === "light"); map!.classList.toggle("forced-light", this.themeMode === "light");
} }
private _loading = false;
private async _loadMap(): Promise<void> { private async _loadMap(): Promise<void> {
if (this._loading) return;
let map = this.shadowRoot!.getElementById("map"); let map = this.shadowRoot!.getElementById("map");
if (!map) { if (!map) {
map = document.createElement("div"); map = document.createElement("div");
map.id = "map"; map.id = "map";
this.shadowRoot!.append(map); this.shadowRoot!.append(map);
} }
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map); this._loading = true;
this._updateMapStyle(); try {
this._loaded = true; [this.leafletMap, this.Leaflet] = await setupLeafletMap(map);
this._updateMapStyle();
this._loaded = true;
} finally {
this._loading = false;
}
} }
public fitMap(options?: { zoom?: number; pad?: number }): void { public fitMap(options?: { zoom?: number; pad?: number }): void {
@ -528,7 +535,6 @@ export class HaMap extends ReactiveElement {
private async _attachObserver(): Promise<void> { private async _attachObserver(): Promise<void> {
if (!this._resizeObserver) { if (!this._resizeObserver) {
await loadPolyfillIfNeeded();
this._resizeObserver = new ResizeObserver(() => { this._resizeObserver = new ResizeObserver(() => {
this.leafletMap?.invalidateSize({ debounceMoveend: true }); this.leafletMap?.invalidateSize({ debounceMoveend: true });
}); });

View File

@ -39,7 +39,6 @@ import {
import { browseLocalMediaPlayer } from "../../data/media_source"; import { browseLocalMediaPlayer } from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts"; import { isTTSMediaSource } from "../../data/tts";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { loadPolyfillIfNeeded } from "../../resources/resize-observer.polyfill";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { import {
@ -770,7 +769,6 @@ export class HaMediaPlayerBrowse extends LitElement {
private async _attachResizeObserver(): Promise<void> { private async _attachResizeObserver(): Promise<void> {
if (!this._resizeObserver) { if (!this._resizeObserver) {
await loadPolyfillIfNeeded();
this._resizeObserver = new ResizeObserver( this._resizeObserver = new ResizeObserver(
debounce(() => this._measureCard(), 250, false) debounce(() => this._measureCard(), 250, false)
); );

View File

@ -79,7 +79,7 @@ class SearchInputOutlined extends LitElement {
} }
private async _filterInputChanged(e) { private async _filterInputChanged(e) {
this._filterChanged(e.target.value); this._filterChanged(e.target.value?.trim());
} }
private async _clearSearch() { private async _clearSearch() {

View File

@ -67,7 +67,7 @@ class SearchInput extends LitElement {
} }
private async _filterInputChanged(e) { private async _filterInputChanged(e) {
this._filterChanged(e.target.value); this._filterChanged(e.target.value?.trim());
} }
private async _clearSearch() { private async _clearSearch() {

View File

@ -13,6 +13,7 @@ import {
mdiClose, mdiClose,
mdiCodeBraces, mdiCodeBraces,
mdiCodeBrackets, mdiCodeBrackets,
mdiFormatListNumbered,
mdiRefresh, mdiRefresh,
mdiRoomService, mdiRoomService,
mdiShuffleDisabled, mdiShuffleDisabled,
@ -29,6 +30,7 @@ import {
ManualScriptConfig, ManualScriptConfig,
ParallelAction, ParallelAction,
RepeatAction, RepeatAction,
SequenceAction,
ServiceAction, ServiceAction,
WaitAction, WaitAction,
WaitForTriggerAction, WaitForTriggerAction,
@ -119,6 +121,7 @@ export class HatScriptGraph extends LitElement {
repeat: this.render_repeat_node, repeat: this.render_repeat_node,
choose: this.render_choose_node, choose: this.render_choose_node,
if: this.render_if_node, if: this.render_if_node,
sequence: this.render_sequence_node,
parallel: this.render_parallel_node, parallel: this.render_parallel_node,
other: this.render_other_node, other: this.render_other_node,
}; };
@ -460,6 +463,44 @@ export class HatScriptGraph extends LitElement {
`; `;
} }
private render_sequence_node(
node: SequenceAction,
path: string,
graphStart = false,
disabled = false
) {
const trace: any = this.trace.trace[path];
return html`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
>
<div class="graph-container" ?track=${path in this.trace.trace}>
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiFormatListNumbered}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
slot="head"
nofocus
></hat-graph-node>
${ensureArray(node.sequence).map((action, i) =>
this.render_action_node(
action,
`${path}/sequence/${i}`,
false,
disabled || node.enabled === false
)
)}
</div>
</hat-graph-branch>
`;
}
private render_parallel_node( private render_parallel_node(
node: ParallelAction, node: ParallelAction,
path: string, path: string,

View File

@ -37,6 +37,7 @@ import {
IfAction, IfAction,
ParallelAction, ParallelAction,
RepeatAction, RepeatAction,
SequenceAction,
getActionType, getActionType,
} from "../../data/script"; } from "../../data/script";
import { describeAction } from "../../data/script_i18n"; import { describeAction } from "../../data/script_i18n";
@ -310,6 +311,10 @@ class ActionRenderer {
return this._handleIf(index); return this._handleIf(index);
} }
if (actionType === "sequence") {
return this._handleSequence(index);
}
if (actionType === "parallel") { if (actionType === "parallel") {
return this._handleParallel(index); return this._handleParallel(index);
} }
@ -579,6 +584,37 @@ class ActionRenderer {
return i; return i;
} }
private _handleSequence(index: number): number {
const sequencePath = this.keys[index];
const sequenceConfig = this._getDataFromPath(
this.keys[index]
) as SequenceAction;
this._renderEntry(
sequencePath,
sequenceConfig.alias ||
describeAction(
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
sequenceConfig,
"sequence"
),
undefined,
sequenceConfig.enabled === false
);
let i: number;
for (i = index + 1; i < this.keys.length; i++) {
const path = this.keys[i];
this._renderItem(i, getActionType(this._getDataFromPath(path)));
}
return i;
}
private _handleParallel(index: number): number { private _handleParallel(index: number): number {
const parallelPath = this.keys[index]; const parallelPath = this.keys[index];
const startLevel = parallelPath.split("/").length; const startLevel = parallelPath.split("/").length;

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