Compare commits

..

37 Commits

Author SHA1 Message Date
Bram Kragten
2ba8f9f99d Bumped version to 20250331.0 2025-03-31 20:43:31 +02:00
Bram Kragten
7e06bbc467 Fix add zwave device my link (#24871) 2025-03-31 20:42:38 +02:00
Paul Bottein
6017d82c21 Handle date range shift during daylight saving time days (#24868) 2025-03-31 20:42:37 +02:00
Bram Kragten
40c200a172 fix spinner in tts try dialog (#24867) 2025-03-31 20:42:36 +02:00
Bram Kragten
a2f70f682f Take lang into account when search existing pipeline (#24866)
* Take lang into account when search existing pipeline

* Simplify logic
2025-03-31 20:42:36 +02:00
Paul Bottein
c42a899b52 Force clock card to display time LTR (#24865) 2025-03-31 20:42:35 +02:00
Paul Bottein
706f43e99e Add interactions for weather card editor (#24864) 2025-03-31 20:42:34 +02:00
karwosts
f5496c21e8 Bar charts start from 0 (#24854) 2025-03-31 20:42:33 +02:00
Paul Bottein
34dce5b279 Only use button for breadcrumb for admin users (#24836) 2025-03-31 20:42:32 +02:00
Bram Kragten
a4f07423ec Name local pipeline based on local or full choice (#24835) 2025-03-31 20:42:31 +02:00
Bram Kragten
9e32c24f3c Update lang support text in voice wizard (#24834) 2025-03-31 20:42:30 +02:00
Paul Bottein
b281d095cd Remove add-on word in satellite wizard translations for state (#24832) 2025-03-31 20:42:29 +02:00
Paul Bottein
fe7e8e17ae More info breadcrumb clickable (#24830)
* Make more info breadcrum clickable

* css adjustements
2025-03-28 15:37:27 +01:00
Eloy Rodriguez
2161357226 Add title and time zone to clock card (#24818)
* Add title and time zone to clock card

* Small changes to the spacing and text sizing of the clock card

* Update translations to use dropdown labels from profile configuration

* Use similar approach as #24819 for setting automatic time zone

* Update hui-clock-card.ts

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-03-28 15:37:02 +01:00
Darren Griffin
e8e65a4293 Fix default time_format option. Fixes #24798 (#24819)
* Fix default time_format option. Fixes #24798

* Update en.json

* Update src/translations/en.json

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-03-28 15:36:29 +01:00
Bram Kragten
724adab2d6 Bumped version to 20250328.0 2025-03-28 15:02:51 +01:00
Bram Kragten
345ad6c9c5 Update voice-assistant-setup-step-local.ts 2025-03-28 15:02:37 +01:00
Bram Kragten
a88d066d7e Update text voice wizard install addons step (#24829) 2025-03-28 15:02:15 +01:00
Paulus Schoutsen
a8e5c8482b Hide backup from default dashboard (#24828) 2025-03-28 15:02:14 +01:00
Paulus Schoutsen
d5ff8ab1e1 Do not play pre-announce sound when testing voice on satellite (#24827) 2025-03-28 15:02:13 +01:00
Bram Kragten
e765cc10fb Fix voice flow (#24825)
* Fix voice flow

* Apply suggestions from code review

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-03-28 15:02:12 +01:00
Paul Bottein
916dec101f Add hold and double tap action in the UI for every card that supports it. (#24824)
* Add double tap action to button card UI editor

* Add double tap action to light card UI editor

* Add hold action and double tap action to gauge card UI editor

* Add hold action and double tap action to picture glance card UI editor

* Add hold action and double tap action to picture card UI editor

* Add hold action and double tap action to entity card UI editor

* Add hold action and double tap action to elements
2025-03-28 15:02:11 +01:00
Paul Bottein
909fc119b7 Add scroll restoration when using back navigation in dashboard (#24822)
Add scroll restoration when using back navigation with subviews
2025-03-28 15:02:10 +01:00
puddly
8751dc46f4 Show hardware integrations in the integration list (#24820)
Show hardware integrations in the frontend
2025-03-28 15:02:09 +01:00
Paul Bottein
118c25d25f
Bumped version to 20250327.1 2025-03-27 19:12:22 +01:00
Paul Bottein
ae5427a75e
Fix dashboard strategy (#24808) 2025-03-27 19:12:03 +01:00
Paul Bottein
3b6e267fb5
Fallback to state name when the entry doesn't have name (#24805) 2025-03-27 19:12:02 +01:00
Bram Kragten
1770a51303 Bumped version to 20250327.0 2025-03-27 16:46:17 +01:00
Paul Bottein
534df3d378 Add loading state to area strategy (#24803) 2025-03-27 16:44:15 +01:00
Paul Bottein
23229b3e3b Set the max number of columns to 3 for area dashboard (#24802)
* Set the max number of columns to 4 for area dashboard

* Set it to 3
2025-03-27 16:44:14 +01:00
karwosts
94ee99160b Energy device settings fixes (#24801) 2025-03-27 16:44:13 +01:00
Paul Bottein
b009d71e8f Fix take control of the dashboard (#24800) 2025-03-27 16:44:12 +01:00
Bram Kragten
2ab8209622 Align behavior of template selector with text selector (#24796) 2025-03-27 16:44:11 +01:00
Paul Bottein
ed2940edc3 Revert "Restore scroll position when using back navigation in dashboard" (#24795)
Revert "Restore scroll position when using back navigation in dashboard (#24777)"

This reverts commit 9cfcd21a93dd50d61fb64039ce7bec973c721806.
2025-03-27 16:44:11 +01:00
Paul Bottein
e2b9a06242 Fix more info for disabled entities (#24789) 2025-03-27 16:44:10 +01:00
Paul Bottein
a7acee0438 Remove fixed height in ha tile info (#24787)
Remove unused height in ha tile info
2025-03-27 16:44:09 +01:00
Bram Kragten
1208af510c Fix typo in Arithmetic (#24786)
Fix type in Arithmetic
2025-03-27 16:44:08 +01:00
209 changed files with 5875 additions and 8711 deletions

View File

@ -26,7 +26,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -62,7 +62,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -60,7 +60,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -78,7 +78,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -102,7 +102,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -27,7 +27,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -63,7 +63,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -34,7 +34,7 @@ jobs:
uses: home-assistant/actions/helpers/verify-version@master
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -74,7 +74,7 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels
uses: home-assistant/wheels@2025.03.0
uses: home-assistant/wheels@2025.02.0
with:
abi: cp313
tag: musllinux_1_2
@ -92,7 +92,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -121,7 +121,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v4.3.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -0,0 +1,34 @@
diff --git a/lib/legacy/class.js b/lib/legacy/class.js
index aee2511be1cd9bf900ee552bc98190c1631c57c0..f2f499d68bf52034cac9c28307c99e8ce6b8417d 100644
--- a/lib/legacy/class.js
+++ b/lib/legacy/class.js
@@ -304,17 +304,23 @@ function GenerateClassFromInfo(info, Base, behaviors) {
// only proceed if the generated class' prototype has not been registered.
const generatedProto = PolymerGenerated.prototype;
if (!generatedProto.hasOwnProperty(JSCompiler_renameProperty('__hasRegisterFinished', generatedProto))) {
- generatedProto.__hasRegisterFinished = true;
+ // make sure legacy lifecycle is called on the *element*'s prototype
+ // and not the generated class prototype; if the element has been
+ // extended, these are *not* the same.
+ const proto = Object.getPrototypeOf(this);
+ // Only set flag when generated prototype itself is registered,
+ // as this element may be extended from, and needs to run `registered`
+ // on all behaviors on the subclass as well.
+ if (proto === generatedProto) {
+ generatedProto.__hasRegisterFinished = true;
+ }
// ensure superclass is registered first.
super._registered();
// copy properties onto the generated class lazily if we're optimizing,
- if (legacyOptimizations) {
+ if (legacyOptimizations && !Object.hasOwnProperty(generatedProto, '__hasCopiedProperties')) {
+ generatedProto.__hasCopiedProperties = true;
copyPropertiesToProto(generatedProto);
}
- // make sure legacy lifecycle is called on the *element*'s prototype
- // and not the generated class prototype; if the element has been
- // extended, these are *not* the same.
- const proto = Object.getPrototypeOf(this);
let list = lifecycle.beforeRegister;
if (list) {
for (let i=0; i < list.length; i++) {

935
.yarn/releases/yarn-4.7.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@ -2,7 +2,7 @@ import defineProvider from "@babel/helper-define-polyfill-provider";
import { join } from "node:path";
import paths from "../paths.cjs";
const POLYFILL_DIR = join(paths.root_dir, "src/resources/polyfills");
const POLYFILL_DIR = join(paths.polymer_dir, "src/resources/polyfills");
// List of polyfill keys with supported browser targets for the functionality
const polyfillSupport = {

View File

@ -20,16 +20,22 @@ module.exports.ignorePackages = () => [];
// Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ isHassioBuild }) =>
[
// Contains all color definitions for all material color sets.
// We don't use it
require.resolve("@polymer/paper-styles/color.js"),
require.resolve("@polymer/paper-styles/default-theme.js"),
// Loads stuff from a CDN
require.resolve("@polymer/font-roboto/roboto.js"),
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
// Icons in supervisor conflict with icons in HA so we don't load.
isHassioBuild &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
path.resolve(paths.polymer_dir, "src/components/ha-icon.ts")
),
isHassioBuild &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
path.resolve(paths.polymer_dir, "src/components/ha-icon-picker.ts")
),
].filter(Boolean);
@ -44,8 +50,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__HASS_URL__: `\`${
"HASS_URL" in process.env
? process.env.HASS_URL
: // eslint-disable-next-line no-template-curly-in-string
"${location.protocol}//${location.host}"
: "${location.protocol}//${location.host}"
}\``,
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
@ -159,7 +164,7 @@ module.exports.babelOptions = ({
],
],
exclude: [
path.join(paths.root_dir, "src/resources/polyfills"),
path.join(paths.polymer_dir, "src/resources/polyfills"),
...[
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
"@lit-labs/virtualizer/polyfills",
@ -177,7 +182,6 @@ module.exports.babelOptions = ({
include: /\/node_modules\//,
exclude: [
"element-internals-polyfill",
"@shoelace-style",
"@?lit(?:-labs|-element|-html)?",
].map((p) => new RegExp(`/node_modules/${p}/`)),
},

View File

@ -21,7 +21,7 @@ module.exports = {
},
version() {
const version = fs
.readFileSync(path.resolve(paths.root_dir, "pyproject.toml"), "utf8")
.readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8")
.match(/version\W+=\W"(\d{8}\.\d(?:\.dev)?)"/);
if (!version) {
throw Error("Version not found");

View File

@ -169,14 +169,14 @@ const APP_PAGE_ENTRIES = {
gulp.task(
"gen-pages-app-dev",
genPagesDevTask(APP_PAGE_ENTRIES, paths.root_dir, paths.app_output_root)
genPagesDevTask(APP_PAGE_ENTRIES, paths.polymer_dir, paths.app_output_root)
);
gulp.task(
"gen-pages-app-prod",
genPagesProdTask(
APP_PAGE_ENTRIES,
paths.root_dir,
paths.polymer_dir,
paths.app_output_root,
paths.app_output_latest,
paths.app_output_es5

View File

@ -6,8 +6,8 @@ import path from "path";
import paths from "../paths.cjs";
const npmPath = (...parts) =>
path.resolve(paths.root_dir, "node_modules", ...parts);
const polyPath = (...parts) => path.resolve(paths.root_dir, ...parts);
path.resolve(paths.polymer_dir, "node_modules", ...parts);
const polyPath = (...parts) => path.resolve(paths.polymer_dir, ...parts);
const copyFileDir = (fromFile, toDir) =>
fs.copySync(fromFile, path.join(toDir, path.basename(fromFile)));

View File

@ -4,7 +4,7 @@ import gulp from "gulp";
import { join, resolve } from "node:path";
import paths from "../paths.cjs";
const formatjsDir = join(paths.root_dir, "node_modules", "@formatjs");
const formatjsDir = join(paths.polymer_dir, "node_modules", "@formatjs");
const outDir = join(paths.build_dir, "locale-data");
const INTL_POLYFILLS = {

View File

@ -1,7 +1,7 @@
const path = require("path");
module.exports = {
root_dir: path.resolve(__dirname, ".."),
polymer_dir: path.resolve(__dirname, ".."),
build_dir: path.resolve(__dirname, "../build"),
app_output_root: path.resolve(__dirname, "../hass_frontend"),

View File

@ -161,7 +161,7 @@ const createRspackConfig = ({
}),
new rspack.NormalModuleReplacementPlugin(
new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
path.resolve(paths.root_dir, "src/util/empty.js")
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
!isProdBuild && new LogStartCompilePlugin(),
isProdBuild &&

View File

@ -3,6 +3,7 @@ export const demoThemeJimpower = () => ({
"paper-item-icon-color": "var(--primary-text-color)",
"primary-color": "#5294E2",
"label-badge-red": "var(--accent-color)",
"paper-tabs-selection-bar-color": "green",
"light-primary-color": "var(--accent-color)",
"primary-background-color": "#383C45",
"primary-text-color": "#FFFFFF",

View File

@ -4,6 +4,7 @@ export const demoThemeKernehed = () => ({
"paper-item-icon-color": "var(--primary-text-color)",
"primary-color": "#2980b9",
"label-badge-red": "var(--accent-color)",
"paper-tabs-selection-bar-color": "green",
"primary-text-color": "#FFFFFF",
"light-primary-color": "var(--accent-color)",
"primary-background-color": "#222222",

View File

@ -42,6 +42,7 @@ export default tseslint.config(
__VERSION__: false,
__STATIC_PATH__: false,
__SUPERVISOR__: false,
Polymer: true,
},
parser: tseslint.parser,

View File

@ -16,14 +16,23 @@ import type { HomeAssistant } from "../../../../src/types";
import type { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
const _filterDevices = memoizeOne(
(hardware: HassioHardwareInfo, filter: string, language: string) =>
(
showAdvanced: boolean,
hardware: HassioHardwareInfo,
filter: string,
language: string
) =>
hardware.devices
.filter(
(device) =>
device.by_id?.toLowerCase().includes(filter) ||
device.name.toLowerCase().includes(filter) ||
device.dev_path.toLocaleLowerCase().includes(filter) ||
JSON.stringify(device.attributes).toLocaleLowerCase().includes(filter)
(showAdvanced ||
["tty", "gpio", "input"].includes(device.subsystem)) &&
(device.by_id?.toLowerCase().includes(filter) ||
device.name.toLowerCase().includes(filter) ||
device.dev_path.toLocaleLowerCase().includes(filter) ||
JSON.stringify(device.attributes)
.toLocaleLowerCase()
.includes(filter))
)
.sort((a, b) => stringCompare(a.name, b.name, language))
);
@ -51,6 +60,7 @@ class HassioHardwareDialog extends LitElement {
}
const devices = _filterDevices(
this.hass.userData?.showAdvanced || false,
this._dialogParams.hardware,
(this._filter || "").toLowerCase(),
this.hass.locale.language

View File

@ -1,6 +1,9 @@
import "./hassio-main";
import("../../src/resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then(
({ setCancelSyntheticClickEvents }) => setCancelSyntheticClickEvents(false)
);
const styleEl = document.createElement("style");
styleEl.textContent = `

View File

@ -26,17 +26,17 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.27.0",
"@babel/runtime": "7.26.10",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/commands": "6.8.1",
"@codemirror/commands": "6.8.0",
"@codemirror/language": "6.11.0",
"@codemirror/legacy-modes": "6.5.0",
"@codemirror/search": "6.5.10",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.36.5",
"@codemirror/view": "6.36.4",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.0",
"@formatjs/intl-datetimeformat": "6.17.4",
"@formatjs/intl-displaynames": "6.8.11",
"@formatjs/intl-durationformat": "0.7.4",
"@formatjs/intl-getcanonicallocales": "2.5.5",
@ -45,12 +45,12 @@
"@formatjs/intl-numberformat": "8.15.4",
"@formatjs/intl-pluralrules": "5.4.4",
"@formatjs/intl-relativetimeformat": "11.4.11",
"@fullcalendar/core": "6.1.17",
"@fullcalendar/daygrid": "6.1.17",
"@fullcalendar/interaction": "6.1.17",
"@fullcalendar/list": "6.1.17",
"@fullcalendar/luxon3": "6.1.17",
"@fullcalendar/timegrid": "6.1.17",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
"@fullcalendar/list": "6.1.15",
"@fullcalendar/luxon3": "6.1.15",
"@fullcalendar/timegrid": "6.1.15",
"@lezer/highlight": "1.2.1",
"@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.8",
@ -81,16 +81,20 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "2.3.0",
"@material/web": "2.2.0",
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1",
"@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.2",
"@replit/codemirror-indentation-markers": "6.5.3",
"@shoelace-style/shoelace": "2.20.1",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.8.1",
"@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.7.3",
"@vaadin/vaadin-themable-mixin": "24.7.3",
"@vaadin/combo-box": "24.7.1",
"@vaadin/vaadin-themable-mixin": "24.7.1",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
@ -107,12 +111,12 @@
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"echarts": "5.6.0",
"element-internals-polyfill": "3.0.2",
"element-internals-polyfill": "3.0.1",
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.5.0",
"home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.7.16",
"js-yaml": "4.1.0",
@ -121,8 +125,8 @@
"leaflet.markercluster": "1.5.3",
"lit": "2.8.0",
"lit-html": "2.8.0",
"luxon": "3.6.1",
"marked": "15.0.8",
"luxon": "3.5.0",
"marked": "15.0.7",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
@ -155,15 +159,15 @@
"@babel/plugin-proposal-decorators": "7.25.9",
"@babel/plugin-transform-runtime": "7.26.10",
"@babel/preset-env": "7.26.9",
"@babel/preset-typescript": "7.27.0",
"@babel/preset-typescript": "7.26.0",
"@bundle-stats/plugin-webpack-filter": "4.19.1",
"@lokalise/node-api": "14.3.0",
"@octokit/auth-oauth-device": "7.1.5",
"@octokit/plugin-retry": "7.2.1",
"@lokalise/node-api": "14.2.0",
"@octokit/auth-oauth-device": "7.1.4",
"@octokit/plugin-retry": "7.2.0",
"@octokit/rest": "21.1.1",
"@rsdoctor/rspack-plugin": "1.0.1",
"@rspack/cli": "1.3.5",
"@rspack/core": "1.3.5",
"@rsdoctor/rspack-plugin": "1.0.0",
"@rspack/cli": "1.2.8",
"@rspack/core": "1.2.8",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.21",
"@types/chromecast-caf-sender": "1.0.11",
@ -175,24 +179,24 @@
"@types/leaflet-draw": "1.0.11",
"@types/leaflet.markercluster": "1.5.5",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.6.2",
"@types/luxon": "3.4.2",
"@types/mocha": "10.0.10",
"@types/qrcode": "1.5.5",
"@types/sortablejs": "1.15.8",
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "3.1.1",
"@vitest/coverage-v8": "3.0.9",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.24.0",
"eslint": "9.23.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.2",
"eslint-config-prettier": "10.1.1",
"eslint-import-resolver-webpack": "0.13.10",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-lit": "2.1.1",
"eslint-plugin-lit": "2.0.0",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "3.0.0",
@ -205,9 +209,9 @@
"gulp-rename": "2.0.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "26.1.0",
"jsdom": "26.0.0",
"jszip": "3.10.1",
"lint-staged": "15.5.1",
"lint-staged": "15.5.0",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@ -216,27 +220,29 @@
"prettier": "3.5.3",
"rspack-manifest-plugin": "5.0.3",
"serve": "14.2.4",
"sinon": "20.0.0",
"sinon": "19.0.4",
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.8.3",
"typescript-eslint": "8.30.1",
"typescript": "5.8.2",
"typescript-eslint": "8.27.0",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.1.1",
"vitest": "3.0.9",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.5.2#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@material/mwc-button@^0.25.3": "^0.27.0",
"lit": "2.8.0",
"lit-html": "2.8.0",
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.17",
"@fullcalendar/daygrid": "6.1.15",
"globals": "16.0.0",
"tslib": "2.8.1"
},
"packageManager": "yarn@4.9.1"
"packageManager": "yarn@4.7.0"
}

View File

@ -1,15 +0,0 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M30.8239 22.3365L38.8239 38.8365L30.3239 50.3365" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
<mask id="mask0_1110_23734" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1110_23734)">
<rect x="30" y="27" width="18" height="18" fill="#212121"/>
</g>
<path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
<circle cx="39" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,15 +0,0 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.824 22.3365L38.824 38.8365L30.324 50.3365" stroke="white" stroke-opacity="0.24" stroke-width="3" stroke-linecap="round"/>
<mask id="mask0_1180_4955" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1180_4955)">
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
</g>
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="white" fill-opacity="0.48"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="white" fill-opacity="0.48"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="white" stroke-opacity="0.24" stroke-width="3" stroke-linecap="round"/>
<circle cx="39" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,19 +0,0 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="47" cy="36" r="34" fill="white"/>
<circle cx="47" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_1110_23775" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1110_23775)">
<rect x="38" y="27" width="18" height="18" fill="#212121"/>
</g>
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,19 +0,0 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="47" cy="36" r="34" fill="#1C1C1C"/>
<circle cx="47" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_1180_4965" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1180_4965)">
<rect x="38" y="27" width="18" height="18" fill="#00AFFF"/>
</g>
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20250326.0"
version = "20250331.0"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"

View File

@ -265,10 +265,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
);
}
if (
window.innerWidth > 450 &&
!matchMedia("(prefers-reduced-motion)").matches
) {
if (window.innerWidth > 450) {
import("../resources/particles");
}

View File

@ -84,12 +84,12 @@ export const calcDateRange = (
case "now-7d":
return [
calcDate(today, subDays, hass.locale, hass.config, 7),
calcDate(today, subDays, hass.locale, hass.config, 0),
calcDate(today, subDays, hass.locale, hass.config, 1),
];
case "now-30d":
return [
calcDate(today, subDays, hass.locale, hass.config, 30),
calcDate(today, subDays, hass.locale, hass.config, 0),
calcDate(today, subDays, hass.locale, hass.config, 1),
];
case "now-12m":
return [

View File

@ -134,7 +134,10 @@ export const applyThemesOnElement = (
element.__themes = { cacheKey, keys: newTheme?.keys };
// Set and/or reset styles
if (window.ShadyCSS) {
if (element.updateStyles) {
// Use updateStyles() method of Polymer elements
element.updateStyles(styles);
} else if (window.ShadyCSS) {
// Use ShadyCSS if available
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
} else {

View File

@ -1,4 +1,3 @@
import memoizeOne from "memoize-one";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type {
EntityRegistryDisplayEntry,
@ -6,7 +5,6 @@ import type {
} from "../../data/entity_registry";
import type { HomeAssistant } from "../../types";
import { computeStateName } from "./compute_state_name";
import { getDuplicates } from "../string/get_duplicates";
export const computeDeviceName = (
device: DeviceRegistryEntry
@ -38,13 +36,3 @@ export const fallbackDeviceName = (
}
return undefined;
};
export const getDuplicatedDeviceNames = memoizeOne(
(devices: HomeAssistant["devices"]): Set<string> => {
const names = Object.values(devices)
.map((device) => computeDeviceName(device))
.filter((name): name is string => name !== undefined);
return getDuplicates(names);
}
);

View File

@ -5,7 +5,7 @@ import { getIntegrationDescriptions } from "../../data/integrations";
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { showMatterAddDeviceDialog } from "../../panels/config/integrations/integration-panels/matter/show-dialog-add-matter-device";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/add-node/show-dialog-zwave_js-add-node";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { isComponentLoaded } from "../config/is_component_loaded";

View File

@ -1,14 +0,0 @@
export function getDuplicates(array: string[]): Set<string> {
const duplicates = new Set<string>();
const seen = new Set<string>();
for (const item of array) {
if (seen.has(item)) {
duplicates.add(item);
} else {
seen.add(item);
}
}
return duplicates;
}

View File

@ -645,16 +645,15 @@ export class HaDataTable extends LitElement {
return;
}
const prom =
this.sortColumn && this._sortColumns[this.sortColumn]
? sortData(
filteredData,
this._sortColumns[this.sortColumn],
this.sortDirection,
this.sortColumn,
this.hass.locale.language
)
: filteredData;
const prom = this.sortColumn
? sortData(
filteredData,
this._sortColumns[this.sortColumn],
this.sortDirection,
this.sortColumn,
this.hass.locale.language
)
: filteredData;
const [data] = await Promise.all([prom, nextRender]);

View File

@ -1,7 +1,7 @@
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html, nothing } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
@ -19,7 +19,7 @@ import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box-item";
import "../ha-list-item";
interface Device {
name: string;
@ -35,14 +35,11 @@ export type HaDevicePickerDeviceFilterFunc = (
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.name}</span>
${item.area
? html`<span slot="supporting-text">${item.area}</span>`
: nothing}
</ha-combo-box-item>
`;
const rowRenderer: ComboBoxLitRenderer<Device> = (item) =>
html`<ha-list-item .twoline=${!!item.area}>
<span>${item.name}</span>
<span slot="secondary">${item.area}</span>
</ha-list-item>`;
@customElement("ha-device-picker")
export class HaDevicePicker extends LitElement {

View File

@ -1,78 +1,35 @@
import { mdiMagnify, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import "../ha-list-item";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { html, LitElement } from "lit";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityContext } from "../../common/entity/get_entity_context";
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
import type { ValueChangedEvent, HomeAssistant } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { computeRTL } from "../../common/util/compute_rtl";
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
import { domainToName } from "../../data/integration";
import type { HelperDomain } from "../../panels/config/helpers/const";
import { isHelperDomain } from "../../panels/config/helpers/const";
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box-item";
import "../ha-icon-button";
import "../ha-list-item";
import "../ha-svg-icon";
import "./state-badge";
const FAKE_ENTITY: HassEntity = {
entity_id: "",
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
attributes: {},
};
interface EntityPickerItem extends HassEntity {
label: string;
primary: string;
secondary?: string;
translated_domain?: string;
show_entity_id?: boolean;
entity_name?: string;
area_name?: string;
device_name?: string;
friendly_name?: string;
sorting_label?: string;
icon_path?: string;
interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
friendly_name: string;
}
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
const CREATE_ID = "___create-new-entity___";
const DOMAIN_STYLE = styleMap({
fontSize: "12px",
fontWeight: "400",
lineHeight: "18px",
alignSelf: "flex-end",
maxWidth: "30%",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
});
const ENTITY_ID_STYLE = styleMap({
fontFamily: "var(--code-font-family, monospace)",
fontSize: "11px",
});
@customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -149,7 +106,8 @@ export class HaEntityPicker extends LitElement {
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: "item-label-path" }) public itemLabelPath = "label";
@property({ attribute: "item-label-path" }) public itemLabelPath =
"friendly_name";
@state() private _opened = false;
@ -165,48 +123,30 @@ export class HaEntityPicker extends LitElement {
await this.comboBox?.focus();
}
private _initialItems = false;
private _initedStates = false;
private _items: EntityPickerItem[] = [];
private _states: HassEntityWithCachedName[] = [];
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.hass.loadBackendTranslation("title");
}
private _rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (
item
) =>
html`<ha-list-item graphic="avatar" .twoline=${!!item.entity_id}>
${item.state
? html`<state-badge
slot="graphic"
.stateObj=${item}
.hass=${this.hass}
></state-badge>`
: ""}
<span>${item.friendly_name}</span>
<span slot="secondary"
>${item.entity_id.startsWith(CREATE_ID)
? this.hass.localize("ui.components.entity.entity-picker.new_entity")
: item.entity_id}</span
>
</ha-list-item>`;
private _rowRenderer: ComboBoxLitRenderer<EntityPickerItem> = (
item,
{ index }
) => html`
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${item.icon_path
? html`<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>`
: html`
<state-badge
slot="start"
.stateObj=${item}
.hass=${this.hass}
></state-badge>
`}
<span slot="headline">${item.primary} </span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.entity_id && item.show_entity_id
? html`<span slot="supporting-text" style=${ENTITY_ID_STYLE}
>${item.entity_id}</span
>`
: nothing}
${item.translated_domain && !item.show_entity_id
? html`<div slot="trailing-supporting-text" style=${DOMAIN_STYLE}>
${item.translated_domain}
</div>`
: nothing}
</ha-combo-box-item>
`;
private _getItems = memoizeOne(
private _getStates = memoizeOne(
(
_opened: boolean,
hass: this["hass"],
@ -218,8 +158,8 @@ export class HaEntityPicker extends LitElement {
includeEntities: this["includeEntities"],
excludeEntities: this["excludeEntities"],
createDomains: this["createDomains"]
): EntityPickerItem[] => {
let states: EntityPickerItem[] = [];
): HassEntityWithCachedName[] => {
let states: HassEntityWithCachedName[] = [];
if (!hass) {
return [];
@ -228,7 +168,7 @@ export class HaEntityPicker extends LitElement {
const createItems = createDomains?.length
? createDomains.map((domain) => {
const primary = hass.localize(
const newFriendlyName = hass.localize(
"ui.components.entity.entity-picker.create_helper",
{
domain: isHelperDomain(domain)
@ -240,14 +180,16 @@ export class HaEntityPicker extends LitElement {
);
return {
...FAKE_ENTITY,
entity_id: CREATE_ID + domain,
primary: primary,
label: primary,
secondary: this.hass.localize(
"ui.components.entity.entity-picker.new_entity"
),
icon_path: mdiPlus,
state: "on",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
friendly_name: newFriendlyName,
attributes: {
icon: "mdi:plus",
},
strings: [domain, newFriendlyName],
};
})
: [];
@ -255,14 +197,21 @@ export class HaEntityPicker extends LitElement {
if (!entityIds.length) {
return [
{
...FAKE_ENTITY,
primary: this.hass!.localize(
entity_id: "",
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
label: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
icon_path: mdiMagnify,
attributes: {
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
icon: "mdi:magnify",
},
strings: [],
},
...createItems,
];
@ -292,49 +241,19 @@ export class HaEntityPicker extends LitElement {
);
}
const isRTL = computeRTL(this.hass);
states = entityIds
.map<EntityPickerItem>((entityId) => {
const stateObj = hass!.states[entityId];
const { area, device } = getEntityContext(stateObj, hass);
const friendlyName = computeStateName(stateObj); // Keep this for search
const entityName = computeEntityName(stateObj, hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const translatedDomain = domainToName(
this.hass.localize,
computeDomain(entityId)
);
.map((key) => {
const friendly_name = computeStateName(hass!.states[key]) || key;
return {
...hass!.states[entityId],
primary: primary,
secondary:
secondary ||
this.hass.localize("ui.components.device-picker.no_area"),
label: friendlyName,
translated_domain: translatedDomain,
sorting_label: [deviceName, entityName].filter(Boolean).join("-"),
entity_name: entityName || deviceName,
area_name: areaName,
device_name: deviceName,
friendly_name: friendlyName,
show_entity_id: hass.userData?.showEntityIdPicker,
...hass!.states[key],
friendly_name,
strings: [key, friendly_name],
};
})
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.sorting_label!,
entityB.sorting_label!,
entityA.friendly_name,
entityB.friendly_name,
this.hass.locale.language
)
);
@ -372,14 +291,21 @@ export class HaEntityPicker extends LitElement {
if (!states.length) {
return [
{
...FAKE_ENTITY,
primary: this.hass!.localize(
entity_id: "",
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
label: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
icon_path: mdiMagnify,
attributes: {
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
icon: "mdi:magnify",
},
strings: [],
},
...createItems,
];
@ -405,8 +331,8 @@ export class HaEntityPicker extends LitElement {
}
public willUpdate(changedProps: PropertyValues) {
if (!this._initialItems || (changedProps.has("_opened") && this._opened)) {
this._items = this._getItems(
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
this._states = this._getStates(
this._opened,
this.hass,
this.includeDomains,
@ -418,10 +344,10 @@ export class HaEntityPicker extends LitElement {
this.excludeEntities,
this.createDomains
);
if (this._initialItems) {
this.comboBox.filteredItems = this._items;
if (this._initedStates) {
this.comboBox.filteredItems = this._states;
}
this._initialItems = true;
this._initedStates = true;
}
if (changedProps.has("createDomains") && this.createDomains?.length) {
@ -441,11 +367,10 @@ export class HaEntityPicker extends LitElement {
: this.label}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._items}
.filteredItems=${this._states}
.renderer=${this._rowRenderer}
.required=${this.required}
.disabled=${this.disabled}
.hideClearIcon=${this.hideClearIcon}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
@ -482,49 +407,12 @@ export class HaEntityPicker extends LitElement {
}
}
private _fuseKeys = [
"entity_name",
"device_name",
"area_name",
"translated_domain",
"friendly_name", // for backwards compatibility
"entity_id", // for technical search
];
private _fuseIndex = memoizeOne((states: EntityPickerItem[]) =>
Fuse.createIndex(this._fuseKeys, states)
);
private _filterChanged(ev: CustomEvent): void {
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.trim().toLowerCase() as string;
const minLength = 2;
const searchTerms = (filterString.split(" ") ?? []).filter(
(term) => term.length >= minLength
);
if (searchTerms.length > 0) {
const index = this._fuseIndex(this._items);
const options: IFuseOptions<EntityPickerItem> = {
isCaseSensitive: false,
threshold: 0.3,
ignoreDiacritics: true,
minMatchCharLength: minLength,
};
const fuse = new Fuse(this._items, options, index);
const results = fuse.search({
$and: searchTerms.map((term) => ({
$or: this._fuseKeys.map((key) => ({ [key]: term })),
})),
});
target.filteredItems = results.map((result) => result.item);
} else {
target.filteredItems = this._items;
}
const filterString = ev.detail.value.trim().toLowerCase();
target.filteredItems = filterString.length
? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states)
: this._states;
}
private _setValue(value: string | undefined) {

View File

@ -1,23 +1,23 @@
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import "@material/mwc-list/mwc-list-item";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
import type { StatisticsMetaData } from "../../data/recorder";
import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import type { ValueChangedEvent, HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box-item";
import "../ha-svg-icon";
import "./state-badge";
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
interface StatisticItem extends ScorableTextItem {
id: string;
@ -99,18 +99,16 @@ export class HaStatisticPicker extends LitElement {
@state() private _filteredItems?: StatisticItem[] = undefined;
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (item) =>
html`<ha-combo-box-item type="button">
html`<mwc-list-item graphic="avatar" twoline>
${item.state
? html`
<state-badge
slot="start"
.stateObj=${item.state}
.hass=${this.hass}
></state-badge>
`
: html`<span slot="start" style="width: 32px"></span>`}
<span slot="headline">${item.name}</span>
<span slot="supporting-text"
? html`<state-badge
slot="graphic"
.stateObj=${item.state}
.hass=${this.hass}
></state-badge>`
: ""}
<span>${item.name}</span>
<span slot="secondary"
>${item.id === "" || item.id === "__missing"
? html`<a
target="_blank"
@ -122,7 +120,7 @@ export class HaStatisticPicker extends LitElement {
>`
: item.id}</span
>
</ha-combo-box-item>`;
</mwc-list-item>`;
private _getStatistics = memoizeOne(
(

View File

@ -79,17 +79,6 @@ export class StateBadge extends LitElement {
</div>`;
}
const cls = this.getClass();
if (cls) {
cls.forEach((toSet, className) => {
if (!toSet) {
this.classList.remove(className);
} else {
this.classList.add(className);
}
});
}
if (!this.icon) {
return nothing;
}
@ -186,57 +175,35 @@ export class StateBadge extends LitElement {
backgroundImage = `url(${imageUrl})`;
this.icon = false;
}
if (domain === "update") {
this.style.borderRadius = "0";
} else if (domain === "media_player" || domain === "camera") {
this.style.borderRadius = "8%";
}
}
this._iconStyle = iconStyle;
this.style.backgroundImage = backgroundImage;
}
protected getClass() {
const cls = new Map(
["has-no-radius", "has-media-image", "has-image"].map((_cls) => [
_cls,
false,
])
);
if (this.stateObj) {
const domain = computeDomain(this.stateObj.entity_id);
if (domain === "update") {
cls.set("has-no-radius", true);
} else if (domain === "media_player" || domain === "camera") {
cls.set("has-media-image", true);
} else if (this.style.backgroundImage !== "") {
cls.set("has-image", true);
}
}
return cls;
}
static get styles(): CSSResultGroup {
return [
iconColorCSS,
css`
:host {
position: relative;
display: inline-flex;
display: inline-block;
width: 40px;
color: var(--paper-item-icon-color, #44739e);
border-radius: var(--state-badge-border-radius, 50%);
border-radius: 50%;
height: 40px;
text-align: center;
background-size: cover;
line-height: 40px;
vertical-align: middle;
box-sizing: border-box;
--state-inactive-color: initial;
align-items: center;
justify-content: center;
}
:host(.has-image) {
border-radius: var(--state-badge-with-image-border-radius, 50%);
}
:host(.has-media-image) {
border-radius: var(--state-badge-with-media-image-border-radius, 8%);
}
:host(.has-no-radius) {
border-radius: 0;
}
:host(:focus) {
outline: none;

View File

@ -10,23 +10,20 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-alert";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-list-item";
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.name}</span>
<span slot="supporting-text">${item.slug}</span>
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) =>
html`<ha-list-item twoline graphic="icon">
<span>${item.name}</span>
<span slot="secondary">${item.slug}</span>
${item.icon
? html`
<img
alt=""
slot="start"
.src="/api/hassio/addons/${item.slug}/icon"
/>
`
: nothing}
</ha-combo-box-item>
`;
? html`<img
alt=""
slot="graphic"
.src="/api/hassio/addons/${item.slug}/icon"
/>`
: ""}
</ha-list-item>`;
@customElement("ha-addon-picker")
class HaAddonPicker extends LitElement {

View File

@ -25,7 +25,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-floor-icon";
import "./ha-icon-button";
import "./ha-list-item";
@ -126,38 +125,38 @@ export class HaAreaFloorPicker extends LitElement {
private _rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) => {
const rtl = computeRTL(this.hass);
return html`
<ha-combo-box-item
type="button"
<ha-list-item
graphic="icon"
style=${item.type === "area" && item.hasFloor
? "--md-list-item-leading-space: 48px;"
? rtl
? "--mdc-list-side-padding-right: 48px;"
: "--mdc-list-side-padding-left: 48px;"
: ""}
>
${item.type === "area" && item.hasFloor
? html`
<ha-tree-indicator
style=${styleMap({
width: "48px",
position: "absolute",
top: "0px",
left: rtl ? undefined : "4px",
right: rtl ? "4px" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${item.lastArea}
slot="start"
></ha-tree-indicator>
`
? html`<ha-tree-indicator
style=${styleMap({
width: "48px",
position: "absolute",
top: "0px",
left: rtl ? undefined : "8px",
right: rtl ? "8px" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${item.lastArea}
slot="graphic"
></ha-tree-indicator>`
: nothing}
${item.type === "floor"
? html`<ha-floor-icon slot="start" .floor=${item}></ha-floor-icon>`
? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>`
: item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
slot="graphic"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${item.name}
</ha-combo-box-item>
</ha-list-item>
`;
};

View File

@ -4,6 +4,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
@ -23,21 +24,22 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-svg-icon";
type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) => html`
<ha-combo-box-item type="button">
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) =>
html`<ha-list-item
graphic="icon"
class=${classMap({ "add-new": item.area_id === ADD_NEW_ID })}
>
${item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTextureBox}></ha-svg-icon>`}
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon slot="graphic" .path=${mdiTextureBox}></ha-svg-icon>`}
${item.name}
</ha-combo-box-item>
`;
</ha-list-item>`;
const ADD_NEW_ID = "___ADD_NEW___";
const NO_ITEMS_ID = "___NO_ITEMS___";

View File

@ -1,7 +1,6 @@
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeStateName } from "../common/entity/compute_state_name";
import { supportsFeature } from "../common/entity/supports-feature";
@ -33,10 +32,6 @@ export class HaCameraStream extends LitElement {
@property({ attribute: false }) public stateObj?: CameraEntity;
@property({ attribute: false }) public aspectRatio?: number;
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
@property({ type: Boolean, attribute: "controls" })
public controls = false;
@ -106,10 +101,6 @@ export class HaCameraStream extends LitElement {
: this._connected
? computeMJPEGStreamUrl(this.stateObj)
: this._posterUrl || ""}
style=${styleMap({
aspectRatio: this.aspectRatio,
objectFit: this.fitMode,
})}
alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
/>`;
}
@ -126,8 +117,6 @@ export class HaCameraStream extends LitElement {
.posterUrl=${this._posterUrl}
@streams=${this._handleHlsStreams}
class=${stream.visible ? "" : "hidden"}
.aspectRatio=${this.aspectRatio}
.fitMode=${this.fitMode}
></ha-hls-player>`;
}
@ -142,8 +131,6 @@ export class HaCameraStream extends LitElement {
.posterUrl=${this._posterUrl}
@streams=${this._handleWebRtcStreams}
class=${stream.visible ? "" : "hidden"}
.aspectRatio=${this.aspectRatio}
.fitMode=${this.fitMode}
></ha-web-rtc-player>`;
}
@ -272,16 +259,6 @@ export class HaCameraStream extends LitElement {
width: 100%;
}
ha-web-rtc-player {
width: 100%;
height: 100%;
}
ha-hls-player {
width: 100%;
height: 100%;
}
.hidden {
display: none;
}

View File

@ -1,46 +0,0 @@
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { HaMdListItem } from "./ha-md-list-item";
@customElement("ha-combo-box-item")
export class HaComboBoxItem extends HaMdListItem {
@property({ type: Boolean, reflect: true, attribute: "border-top" })
public borderTop = false;
static override styles = [
...super.styles,
css`
:host {
--md-list-item-one-line-container-height: 48px;
--md-list-item-two-line-container-height: 64px;
}
:host([border-top]) md-item {
border-top: 1px solid var(--divider-color);
}
[slot="start"] {
--paper-item-icon-color: var(--secondary-text-color);
}
[slot="headline"] {
line-height: 22px;
font-size: 14px;
white-space: nowrap;
}
[slot="supporting-text"] {
line-height: 18px;
font-size: 12px;
white-space: nowrap;
}
::slotted(state-badge),
::slotted(img) {
width: 32px;
height: 32px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-combo-box-item": HaComboBoxItem;
}
}

View File

@ -16,8 +16,8 @@ import { customElement, property, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types";
import "./ha-combo-box-item";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@ -105,9 +105,6 @@ export class HaComboBox extends LitElement {
@property({ type: Boolean, reflect: true }) public opened = false;
@property({ type: Boolean, attribute: "hide-clear-icon" })
public hideClearIcon = false;
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
@query("ha-textfield", true) private _inputElement!: HaTextField;
@ -190,7 +187,7 @@ export class HaComboBox extends LitElement {
>
<slot name="icon" slot="leadingIcon"></slot>
</ha-textfield>
${this.value && !this.hideClearIcon
${this.value
? html`<ha-svg-icon
role="button"
tabindex="-1"
@ -207,7 +204,6 @@ export class HaComboBox extends LitElement {
aria-expanded=${this.opened ? "true" : "false"}
class="toggle-button"
.path=${this.opened ? mdiMenuUp : mdiMenuDown}
?disabled=${this.disabled}
@click=${this._toggleOpen}
></ha-svg-icon>
</vaadin-combo-box-light>
@ -216,11 +212,10 @@ export class HaComboBox extends LitElement {
private _defaultRowRenderer: ComboBoxLitRenderer<
string | Record<string, any>
> = (item) => html`
<ha-combo-box-item type="button">
> = (item) =>
html`<ha-list-item>
${this.itemLabelPath ? item[this.itemLabelPath] : item}
</ha-combo-box-item>
`;
</ha-list-item>`;
private _clearValue(ev: Event) {
ev.stopPropagation();
@ -361,10 +356,6 @@ export class HaComboBox extends LitElement {
:host([opened]) .toggle-button {
color: var(--primary-color);
}
.toggle-button[disabled] {
color: var(--disabled-text-color);
pointer-events: none;
}
.clear-button {
--mdc-icon-size: 20px;
top: -7px;

View File

@ -1,3 +1,4 @@
import "@material/mwc-list/mwc-list-item";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@ -10,7 +11,6 @@ import type { ValueChangedEvent, HomeAssistant } from "../types";
import { brandsUrl } from "../util/brands-url";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
export interface ConfigEntryExtended extends ConfigEntry {
localized_domain_name?: string;
@ -48,20 +48,18 @@ class HaConfigEntryPicker extends LitElement {
this._getConfigEntries();
}
private _rowRenderer: ComboBoxLitRenderer<ConfigEntryExtended> = (
item
) => html`
<ha-combo-box-item type="button">
<span slot="headline">
${item.title ||
private _rowRenderer: ComboBoxLitRenderer<ConfigEntryExtended> = (item) =>
html`<mwc-list-item twoline graphic="icon">
<span
>${item.title ||
this.hass.localize(
"ui.panel.config.integrations.config_entry.unnamed_entry"
)}
</span>
<span slot="supporting-text">${item.localized_domain_name}</span>
)}</span
>
<span slot="secondary">${item.localized_domain_name}</span>
<img
alt=""
slot="start"
slot="graphic"
src=${brandsUrl({
domain: item.domain,
type: "icon",
@ -72,8 +70,7 @@ class HaConfigEntryPicker extends LitElement {
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</ha-combo-box-item>
`;
</mwc-list-item>`;
protected render() {
if (!this._configEntries) {

View File

@ -3,6 +3,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
@ -27,9 +28,9 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-floor-icon";
import "./ha-icon-button";
import "./ha-list-item";
type ScorableFloorRegistryEntry = ScorableTextItem & FloorRegistryEntry;
@ -37,12 +38,14 @@ const ADD_NEW_ID = "___ADD_NEW___";
const NO_FLOORS_ID = "___NO_FLOORS___";
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) => html`
<ha-combo-box-item type="button">
<ha-floor-icon slot="start" .floor=${item}></ha-floor-icon>
const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
html`<ha-list-item
graphic="icon"
class=${classMap({ "add-new": item.floor_id === ADD_NEW_ID })}
>
<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>
${item.name}
</ha-combo-box-item>
`;
</ha-list-item>`;
@customElement("ha-floor-picker")
export class HaFloorPicker extends LitElement {

View File

@ -2,13 +2,12 @@ import type HlsType from "hls.js";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status";
import { fetchStreamUrl } from "../data/camera";
import type { HomeAssistant } from "../types";
import "./ha-alert";
import { fetchStreamUrl } from "../data/camera";
import { isComponentLoaded } from "../common/config/is_component_loaded";
type HlsLite = Omit<
HlsType,
@ -25,10 +24,6 @@ class HaHLSPlayer extends LitElement {
@property({ attribute: "poster-url" }) public posterUrl?: string;
@property({ attribute: false }) public aspectRatio?: number;
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
@property({ type: Boolean, attribute: "controls" })
public controls = false;
@ -92,11 +87,6 @@ class HaHLSPlayer extends LitElement {
?playsinline=${this.playsInline}
?controls=${this.controls}
@loadeddata=${this._loadedData}
style=${styleMap({
height: this.aspectRatio == null ? "100%" : "auto",
aspectRatio: this.aspectRatio,
objectFit: this.fitMode,
})}
></video>`
: ""}
`;

View File

@ -5,13 +5,11 @@ import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-md-button-menu";
import "./ha-button-menu";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-svg-icon";
import "./ha-tooltip";
import "./ha-md-menu-item";
import "./ha-md-divider";
export interface IconOverflowMenuItem {
[key: string]: any;
@ -37,9 +35,11 @@ export class HaIconOverflowMenu extends LitElement {
return html`
${this.narrow
? html` <!-- Collapsed representation for small screens -->
<ha-md-button-menu
<ha-button-menu
@click=${this._handleIconOverflowMenuOpened}
positioning="popover"
@closed=${this._handleIconOverflowMenuClosed}
class="ha-icon-overflow-menu-overflow"
absolute
>
<ha-icon-button
.label=${this.hass.localize("ui.common.overflow_menu")}
@ -49,24 +49,23 @@ export class HaIconOverflowMenu extends LitElement {
${this.items.map((item) =>
item.divider
? html`<ha-md-divider
role="separator"
tabindex="-1"
></ha-md-divider>`
: html`<ha-md-menu-item
? html`<li divider role="separator"></li>`
: html`<ha-list-item
graphic="icon"
?disabled=${item.disabled}
.clickAction=${item.action}
@click=${item.action}
class=${classMap({ warning: Boolean(item.warning) })}
>
<ha-svg-icon
slot="start"
class=${classMap({ warning: Boolean(item.warning) })}
.path=${item.path}
></ha-svg-icon>
<div slot="graphic">
<ha-svg-icon
class=${classMap({ warning: Boolean(item.warning) })}
.path=${item.path}
></ha-svg-icon>
</div>
${item.label}
</ha-md-menu-item> `
</ha-list-item> `
)}
</ha-md-button-menu>`
</ha-button-menu>`
: html`
<!-- Icon representation for big screens -->
${this.items.map((item) =>
@ -92,6 +91,20 @@ export class HaIconOverflowMenu extends LitElement {
protected _handleIconOverflowMenuOpened(e) {
e.stopPropagation();
// If this component is used inside a data table, the z-index of the row
// needs to be increased. Otherwise the ha-button-menu would be displayed
// underneath the next row in the table.
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
if (row) {
row.style.zIndex = "1";
}
}
protected _handleIconOverflowMenuClosed() {
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
if (row) {
row.style.zIndex = "";
}
}
static get styles() {
@ -102,10 +115,16 @@ export class HaIconOverflowMenu extends LitElement {
display: flex;
justify-content: flex-end;
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
div[role="separator"] {
border-right: 1px solid var(--divider-color);
width: 1px;
}
ha-list-item[disabled] ha-svg-icon {
color: var(--disabled-text-color);
}
`,
];
}

View File

@ -11,8 +11,8 @@ import { fireEvent } from "../common/dom/fire_event";
import { customIcons } from "../data/custom_icons";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-combo-box";
import "./ha-list-item";
import "./ha-icon";
import "./ha-combo-box-item";
interface IconItem {
icon: string;
@ -67,12 +67,11 @@ const loadCustomIconItems = async (iconsetPrefix: string) => {
}
};
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) => html`
<ha-combo-box-item type="button">
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) =>
html`<ha-list-item graphic="avatar">
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
${item.icon}
</ha-combo-box-item>
`;
</ha-list-item>`;
@customElement("ha-icon-picker")
export class HaIconPicker extends LitElement {

View File

@ -3,6 +3,7 @@ import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
@ -25,8 +26,8 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-svg-icon";
type ScorableLabelItem = ScorableTextItem & LabelRegistryEntry;
@ -35,14 +36,16 @@ const ADD_NEW_ID = "___ADD_NEW___";
const NO_LABELS_ID = "___NO_LABELS___";
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
const rowRenderer: ComboBoxLitRenderer<LabelRegistryEntry> = (item) => html`
<ha-combo-box-item type="button">
const rowRenderer: ComboBoxLitRenderer<LabelRegistryEntry> = (item) =>
html`<ha-list-item
graphic="icon"
class=${classMap({ "add-new": item.label_id === ADD_NEW_ID })}
>
${item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
: nothing}
${item.name}
</ha-combo-box-item>
`;
</ha-list-item>`;
@customElement("ha-label-picker")
export class HaLabelPicker extends SubscribeMixin(LitElement) {

View File

@ -1,14 +1,15 @@
import "@material/mwc-list/mwc-list";
import type { ActionDetail } from "@material/mwc-list/mwc-list";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { navigate } from "../common/navigate";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../types";
import "./ha-icon-next";
import "./ha-list-item";
import "./ha-svg-icon";
import "./ha-md-list";
import "./ha-md-list-item";
@customElement("ha-navigation-list")
class HaNavigationList extends LitElement {
@ -25,21 +26,21 @@ class HaNavigationList extends LitElement {
public render(): TemplateResult {
return html`
<ha-md-list
<mwc-list
innerRole="menu"
itemRoles="menuitem"
innerAriaLabel=${ifDefined(this.label)}
@action=${this._handleListAction}
>
${this.pages.map((page) => {
const externalApp = page.path.endsWith("#external-app-configuration");
return html`
<ha-md-list-item
.type=${externalApp ? "button" : "link"}
.href=${externalApp ? undefined : page.path}
@click=${externalApp ? this._handleExternalApp : undefined}
${this.pages.map(
(page) => html`
<ha-list-item
graphic="avatar"
.twoline=${this.hasSecondary}
.hasMeta=${!this.narrow}
>
<div
slot="start"
slot="graphic"
class=${page.iconColor ? "icon-background" : ""}
.style="background-color: ${page.iconColor || "undefined"}"
>
@ -47,23 +48,31 @@ class HaNavigationList extends LitElement {
</div>
<span>${page.name}</span>
${this.hasSecondary
? html`<span slot="supporting-text">${page.description}</span>`
? html`<span slot="secondary">${page.description}</span>`
: ""}
${!this.narrow
? html`<ha-icon-next slot="end"></ha-icon-next>`
? html`<ha-icon-next slot="meta"></ha-icon-next>`
: ""}
</ha-md-list-item>
`;
})}
</ha-md-list>
</ha-list-item>
`
)}
</mwc-list>
`;
}
private _handleExternalApp() {
this.hass.auth.external!.fireMessage({ type: "config_screen/show" });
private _handleListAction(ev: CustomEvent<ActionDetail>) {
const path = this.pages[ev.detail.index].path;
if (path.endsWith("#external-app-configuration")) {
this.hass.auth.external!.fireMessage({ type: "config_screen/show" });
} else {
navigate(path);
}
}
static styles: CSSResultGroup = css`
:host {
--mdc-list-vertical-padding: 0;
}
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
@ -80,7 +89,8 @@ class HaNavigationList extends LitElement {
.icon-background ha-svg-icon {
color: #fff;
}
ha-md-list-item {
ha-list-item {
cursor: pointer;
font-size: var(--navigation-list-item-title-font-size);
}
`;

View File

@ -1,6 +1,7 @@
import "@material/mwc-list/mwc-list-item";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { titleCase } from "../common/string/title-case";
@ -9,7 +10,6 @@ import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-icon";
interface NavigationItem {
@ -21,13 +21,11 @@ interface NavigationItem {
const DEFAULT_ITEMS: NavigationItem[] = [];
const rowRenderer: ComboBoxLitRenderer<NavigationItem> = (item) => html`
<ha-combo-box-item type="button">
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
<span slot="headline">${item.title || item.path}</span>
${item.title
? html`<span slot="supporting-text">${item.path}</span>`
: nothing}
</ha-combo-box-item>
<mwc-list-item graphic="icon" .twoline=${!!item.title}>
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
<span>${item.title || item.path}</span>
<span slot="secondary">${item.path}</span>
</mwc-list-item>
`;
const createViewNavigationItem = (

View File

@ -1,5 +1,7 @@
import "@material/mwc-button/mwc-button";
import { mdiCamera } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
// The BarcodeDetector Web API is not yet supported in all browsers,
@ -10,13 +12,12 @@ import { prepareZXingModule } from "barcode-detector";
import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import type { LocalizeFunc } from "../common/translations/localize";
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
import type { HomeAssistant } from "../types";
import "./ha-alert";
import "./ha-button";
import "./ha-button-menu";
import "./ha-list-item";
import "./ha-spinner";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@ -35,22 +36,18 @@ prepareZXingModule({
class HaQrScanner extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public localize!: LocalizeFunc;
@property() public description?: string;
@property({ attribute: "alternative_option_label" })
public alternativeOptionLabel?: string;
@property({ attribute: false }) public validate?: (
value: string
) => string | undefined;
@property() public error?: string;
@state() private _cameras?: QrScanner.Camera[];
@state() private _loading = true;
@state() private _error?: string;
@state() private _warning?: string;
@state() private _manual = false;
private _qrScanner?: QrScanner;
@ -91,40 +88,29 @@ class HaQrScanner extends LitElement {
this._loadQrScanner();
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("error") && this.error) {
alert(`error: ${this.error}`);
this._notifyExternalScanner(this.error);
}
}
protected render() {
if (this._nativeBarcodeScanner) {
if (this._nativeBarcodeScanner && !this._manual) {
return nothing;
}
return html`${this._error || this._warning
? html`<ha-alert
.alertType=${this._error ? "error" : "warning"}
class=${this._error ? "" : "warning"}
>
${this._error || this._warning}
${this._error
? html` <ha-button @click=${this._retry} slot="action">
${this.hass.localize("ui.components.qr-scanner.retry")}
</ha-button>`
: nothing}
</ha-alert>`
: nothing}
${navigator.mediaDevices
return html`${this.error
? html`<ha-alert alert-type="error">${this.error}</ha-alert>`
: ""}
${navigator.mediaDevices && !this._manual
? html`<video></video>
<div id="canvas-container">
${this._loading
? html`<div class="loading">
<ha-spinner active></ha-spinner>
</div>`
: nothing}
${!this._loading &&
!this._error &&
this._cameras &&
this._cameras.length > 1
${this._cameras && this._cameras.length > 1
? html`<ha-button-menu fixed @closed=${stopPropagation}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize(
.label=${this.localize(
"ui.components.qr-scanner.select_camera"
)}
.path=${mdiCamera}
@ -142,25 +128,25 @@ class HaQrScanner extends LitElement {
</ha-button-menu>`
: nothing}
</div>`
: html`<ha-alert alert-type="warning">
${!window.isSecureContext
? this.hass.localize(
"ui.components.qr-scanner.only_https_supported"
)
: this.hass.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>
<p>${this.hass.localize("ui.components.qr-scanner.manual_input")}</p>
: html`${this._manual
? nothing
: html`<ha-alert alert-type="warning">
${!window.isSecureContext
? this.localize(
"ui.components.qr-scanner.only_https_supported"
)
: this.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>`}
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
<div class="row">
<ha-textfield
.label=${this.hass.localize(
"ui.components.qr-scanner.enter_qr_code"
)}
.label=${this.localize("ui.components.qr-scanner.enter_qr_code")}
@keyup=${this._manualKeyup}
@paste=${this._manualPaste}
></ha-textfield>
<ha-button @click=${this._manualSubmit}>
${this.hass.localize("ui.common.submit")}
</ha-button>
<mwc-button @click=${this._manualSubmit}>
${this.localize("ui.common.submit")}
</mwc-button>
</div>`}`;
}
@ -179,9 +165,7 @@ class HaQrScanner extends LitElement {
// eslint-disable-next-line @typescript-eslint/naming-convention
const QrScanner = (await import("qr-scanner")).default;
if (!(await QrScanner.hasCamera())) {
this._reportError(
this.hass.localize("ui.components.qr-scanner.no_camera_found")
);
this._reportError("No camera found");
return;
}
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
@ -197,7 +181,6 @@ class HaQrScanner extends LitElement {
canvas.style.display = "block";
try {
await this._qrScanner.start();
this._loading = false;
} catch (err: any) {
this._reportError(err);
}
@ -210,8 +193,8 @@ class HaQrScanner extends LitElement {
private _qrCodeError = (err: any) => {
if (err.endsWith("No QR code found")) {
this._qrNotFoundCount++;
if (this._qrNotFoundCount >= 250) {
this._reportWarning(err);
if (this._qrNotFoundCount === 250) {
this._reportError(err);
}
return;
}
@ -221,17 +204,7 @@ class HaQrScanner extends LitElement {
};
private _qrCodeScanned = (qrCodeString: string): void => {
this._warning = undefined;
this._qrNotFoundCount = 0;
if (this.validate) {
const validationMessage = this.validate(qrCodeString);
if (validationMessage) {
this._reportWarning(validationMessage);
return;
}
}
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
};
@ -261,10 +234,7 @@ class HaQrScanner extends LitElement {
if (msg.command === "bar_code/scan_result") {
if (msg.payload.format !== "qr_code") {
this._notifyExternalScanner(
this.hass.localize("ui.components.qr-scanner.wrong_code", {
format: msg.payload.format,
rawValue: msg.payload.rawValue,
})
`Wrong barcode scanned! ${msg.payload.format}: ${msg.payload.rawValue}, we need a QR code.`
);
} else {
this._qrCodeScanned(msg.payload.rawValue);
@ -274,7 +244,7 @@ class HaQrScanner extends LitElement {
if (msg.payload.reason === "canceled") {
fireEvent(this, "qr-code-closed");
} else {
fireEvent(this, "qr-code-more-options");
this._manual = true;
}
}
return true;
@ -282,17 +252,10 @@ class HaQrScanner extends LitElement {
this.hass.auth.external!.fireMessage({
type: "bar_code/scan",
payload: {
title:
this.title ||
this.hass.localize("ui.components.qr-scanner.app.title"),
description:
this.description ||
this.hass.localize("ui.components.qr-scanner.app.description"),
title: this.title || "Scan QR code",
description: this.description || "Scan a barcode.",
alternative_option_label:
this.alternativeOptionLabel ||
this.hass.localize(
"ui.components.qr-scanner.app.alternativeOptionLabel"
),
this.alternativeOptionLabel || "Click to manually enter the barcode",
},
});
}
@ -306,55 +269,25 @@ class HaQrScanner extends LitElement {
}
private _notifyExternalScanner(message: string) {
if (!this._nativeBarcodeScanner) {
if (!this.hass.auth.external) {
return;
}
this.hass.auth.external!.fireMessage({
this.hass.auth.external.fireMessage({
type: "bar_code/notify",
payload: {
message,
},
});
this._warning = undefined;
this._error = undefined;
this.error = undefined;
}
private _reportError(message: string) {
const canvas = this._qrScanner?.$canvas;
if (canvas) {
canvas.style.display = "none";
}
this._error = message;
}
private _reportWarning(message: string) {
if (this._nativeBarcodeScanner) {
this._notifyExternalScanner(message);
} else {
this._warning = message;
}
}
private async _retry() {
if (this._qrScanner) {
this._loading = true;
this._error = undefined;
this._warning = undefined;
const canvas = this._qrScanner.$canvas;
canvas.style.display = "block";
this._qrNotFoundCount = 0;
await this._qrScanner.start();
this._loading = false;
}
fireEvent(this, "qr-code-error", { message });
}
static styles = css`
:root {
position: relative;
}
canvas {
width: 100%;
border-radius: 16px;
}
#canvas-container {
position: relative;
@ -379,24 +312,6 @@ class HaQrScanner extends LitElement {
margin-inline-end: 8px;
margin-inline-start: initial;
}
.loading {
display: flex;
position: absolute;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
ha-alert {
display: block;
}
ha-alert.warning {
position: absolute;
z-index: 1;
background-color: var(--primary-background-color);
top: 0;
width: calc(100% - 48px);
}
`;
}
@ -404,8 +319,8 @@ declare global {
// for fire event
interface HASSDomEvents {
"qr-code-scanned": { value: string };
"qr-code-error": { message: string };
"qr-code-closed": undefined;
"qr-code-more-options": undefined;
}
interface HTMLElementTagNameMap {

View File

@ -3,7 +3,7 @@ import {
mdiAlertCircleOutline,
mdiDevices,
mdiPaletteSwatch,
mdiTextureBox,
mdiSofa,
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
@ -235,15 +235,10 @@ export class HaRelatedItems extends LitElement {
})}
slot="graphic"
></div>`
: area.icon
? html`<ha-icon
slot="graphic"
.icon=${area.icon}
></ha-icon>`
: html`<ha-svg-icon
slot="graphic"
.path=${mdiTextureBox}
></ha-svg-icon>`}
: html`<ha-svg-icon
.path=${mdiSofa}
slot="graphic"
></ha-svg-icon>`}
${area.name}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>

View File

@ -83,10 +83,6 @@ export class HaBackgroundSelector extends LitElement {
display: block;
position: relative;
}
ha-picture-upload {
background-color: var(--primary-background-color);
border-radius: var(--file-upload-image-border-radius);
}
div {
display: flex;
flex-direction: column;

View File

@ -38,7 +38,6 @@ import "./ha-settings-row";
import "./ha-yaml-editor";
import type { HaYamlEditor } from "./ha-yaml-editor";
import "./ha-service-section-icon";
import { hasTemplate } from "../common/string/has-template";
const attributeFilter = (values: any[], attribute: any) => {
if (typeof attribute === "object") {
@ -102,8 +101,6 @@ export class HaServiceControl extends LitElement {
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _stickySelector: Record<string, Selector> = {};
protected willUpdate(changedProperties: PropertyValues<this>) {
if (!this.hasUpdated) {
this.hass.loadBackendTranslation("services");
@ -593,23 +590,7 @@ export class HaServiceControl extends LitElement {
return nothing;
}
const fieldDataHasTemplate =
this._value?.data && hasTemplate(this._value.data[dataField.key]);
const selector =
fieldDataHasTemplate &&
typeof this._value!.data![dataField.key] === "string"
? { template: null }
: fieldDataHasTemplate &&
typeof this._value!.data![dataField.key] === "object"
? { object: null }
: (this._stickySelector[dataField.key] ??
dataField?.selector ?? { text: null });
if (fieldDataHasTemplate) {
// Hold this selector type until the field is cleared
this._stickySelector[dataField.key] = selector;
}
const selector = dataField?.selector ?? { text: undefined };
const showOptional = showOptionalToggle(dataField);
@ -712,7 +693,6 @@ export class HaServiceControl extends LitElement {
this._checkedKeys.delete(key);
data = { ...this._value?.data };
delete data[key];
delete this._stickySelector[key];
}
if (data) {
fireEvent(this, "value-changed", {
@ -836,10 +816,6 @@ export class HaServiceControl extends LitElement {
private _serviceDataChanged(ev: CustomEvent) {
ev.stopPropagation();
if (ev.detail.isValid === false) {
// Don't clear an object selector that returns invalid YAML
return;
}
const key = (ev.currentTarget as any).key;
const value = ev.detail.value;
if (
@ -852,13 +828,8 @@ export class HaServiceControl extends LitElement {
const data = { ...this._value?.data, [key]: value };
if (
value === "" ||
value === undefined ||
(typeof value === "object" && !Object.keys(value).length)
) {
if (value === "" || value === undefined) {
delete data[key];
delete this._stickySelector[key];
}
fireEvent(this, "value-changed", {

View File

@ -7,7 +7,7 @@ import type { LocalizeFunc } from "../common/translations/localize";
import { domainToName } from "../data/integration";
import type { HomeAssistant } from "../types";
import "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-list-item";
import "./ha-service-icon";
import { getServiceIcons } from "../data/icons";
@ -29,19 +29,18 @@ class HaServicePicker extends LitElement {
}
private _rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> =
(item) => html`
<ha-combo-box-item type="button">
(item) =>
html`<ha-list-item twoline graphic="icon">
<ha-service-icon
slot="start"
slot="graphic"
.hass=${this.hass}
.service=${item.service}
></ha-service-icon>
<span slot="headline">${item.name}</span>
<span slot="supporting-text"
<span>${item.name}</span>
<span slot="secondary"
>${item.name === item.service ? "" : item.service}</span
>
</ha-combo-box-item>
`;
</ha-list-item>`;
protected render() {
return html`

View File

@ -17,16 +17,15 @@ import {
mdiTooltipAccount,
mdiViewDashboard,
} from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResult, CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import {
customElement,
eventOptions,
property,
state,
query,
} from "lit/decorators";
import { customElement, eventOptions, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { storage } from "../common/decorators/storage";
import { fireEvent } from "../common/dom/fire_event";
@ -49,9 +48,7 @@ import "./ha-menu-button";
import "./ha-sortable";
import "./ha-svg-icon";
import "./user/ha-user-badge";
import "./ha-md-list";
import "./ha-md-list-item";
import type { HaMdListItem } from "./ha-md-list-item";
import { preventDefault } from "../common/dom/prevent_default";
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
@ -224,8 +221,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
})
private _hiddenPanels: string[] = [];
@query(".tooltip") private _tooltip!: HTMLDivElement;
public hassSubscribe(): UnsubscribeFunc[] {
return this.hass.user?.is_admin
? [
@ -243,20 +238,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return nothing;
}
// Show the supervisor as being part of configuration
const selectedPanel = this.route.path?.startsWith("/hassio/")
? "config"
: this.hass.panelUrl;
// prettier-ignore
return html`
${this._renderHeader()}
${this._renderAllPanels(selectedPanel)}
${this._renderAllPanels()}
${this._renderDivider()}
<ha-md-list>
${this._renderNotifications()}
${this._renderUserItem(selectedPanel)}
</ha-md-list>
${this._renderNotifications()}
${this._renderUserItem()}
<div disabled class="bottom-spacer"></div>
<div class="tooltip"></div>
`;
@ -326,11 +314,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
this.hass &&
oldHass?.connected === false &&
changedProps.get("hass")?.connected === false &&
this.hass.connected === true
) {
this._subscribePersistentNotifications();
@ -341,8 +327,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
}
if (oldHass?.panelUrl !== this.hass.panelUrl) {
const selectedEl = this.shadowRoot!.querySelector(".selected");
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) {
const selectedEl = this.shadowRoot!.querySelector(".iron-selected");
if (selectedEl) {
// @ts-ignore
selectedEl.scrollIntoViewIfNeeded();
@ -394,7 +381,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
</div>`;
}
private _renderAllPanels(selectedPanel: string) {
private _renderAllPanels() {
const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
@ -403,26 +390,34 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this.hass.locale
);
// Show the supervisor as being part of configuration
const selectedPanel = this.route.path?.startsWith("/hassio/")
? "config"
: this.hass.panelUrl;
// prettier-ignore
return html`
<ha-md-list
<paper-listbox
attr-for-selected="data-panel"
class="ha-scrollbar"
.selected=${selectedPanel}
@focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut}
@scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown}
@iron-activate=${preventDefault}
>
${this.editMode
? this._renderPanelsEdit(beforeSpacer, selectedPanel)
: this._renderPanels(beforeSpacer, selectedPanel)}
? this._renderPanelsEdit(beforeSpacer)
: this._renderPanels(beforeSpacer)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer, selectedPanel)}
${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
</ha-md-list>
</paper-listbox>
`;
}
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
private _renderPanels(panels: PanelInfo[]) {
return panels.map((panel) =>
this._renderPanel(
panel.url_path,
@ -434,8 +429,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
? PANEL_ICONS.lovelace
: panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path]
: undefined,
selectedPanel
: undefined
)
);
}
@ -443,24 +437,30 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _renderPanel(
urlPath: string,
title: string | null,
icon: string | null | undefined,
iconPath: string | null | undefined,
selectedPanel: string
icon?: string | null,
iconPath?: string | null
) {
return urlPath === "config"
? this._renderConfiguration(title, selectedPanel)
? this._renderConfiguration(title)
: html`
<ha-md-list-item
.href=${this.editMode ? undefined : `/${urlPath}`}
type="link"
class=${selectedPanel === urlPath ? "selected" : ""}
<a
role="option"
aria-selected=${urlPath === this.hass.panelUrl}
href=${`/${urlPath}`}
data-panel=${urlPath}
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
${iconPath
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
<span class="item-text" slot="headline">${title}</span>
<paper-icon-item>
${iconPath
? html`<ha-svg-icon
slot="item-icon"
.path=${iconPath}
></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
<span class="item-text">${title}</span>
</paper-icon-item>
${this.editMode
? html`<ha-icon-button
.label=${this.hass.localize("ui.sidebar.hide_panel")}
@ -468,10 +468,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
class="hide-panel"
.panel=${urlPath}
@click=${this._hidePanel}
slot="end"
></ha-icon-button>`
: nothing}
</ha-md-list-item>
: ""}
</a>
`;
}
@ -494,10 +493,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._panelOrder = panelOrder;
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) {
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
return html`
<ha-sortable .disabled=${!this.editMode} @item-moved=${this._panelMoved}
><div>${this._renderPanels(beforeSpacer, selectedPanel)}</div>
<ha-sortable
handle-selector="paper-icon-item"
.disabled=${!this.editMode}
@item-moved=${this._panelMoved}
>
<div class="reorder-list">${this._renderPanels(beforeSpacer)}</div>
</ha-sortable>
${this._renderSpacer()}${this._renderHiddenPanels()}
`;
@ -510,24 +513,26 @@ class HaSidebar extends SubscribeMixin(LitElement) {
if (!panel) {
return "";
}
return html`<ha-md-list-item
return html`<paper-icon-item
@click=${this._unhidePanel}
class="hidden-panel"
.panel=${url}
type="button"
>
${panel.url_path === this.hass.defaultPanel && !panel.icon
? html`<ha-svg-icon
slot="start"
slot="item-icon"
.path=${PANEL_ICONS.lovelace}
></ha-svg-icon>`
: panel.url_path in PANEL_ICONS
? html`<ha-svg-icon
slot="start"
slot="item-icon"
.path=${PANEL_ICONS[panel.url_path]}
></ha-svg-icon>`
: html`<ha-icon slot="start" .icon=${panel.icon}></ha-icon>`}
<span class="item-text" slot="headline"
: html`<ha-icon
slot="item-icon"
.icon=${panel.icon}
></ha-icon>`}
<span class="item-text"
>${panel.url_path === this.hass.defaultPanel
? this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) ||
@ -537,9 +542,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
.label=${this.hass.localize("ui.sidebar.show_panel")}
.path=${mdiPlus}
class="show-panel"
slot="end"
></ha-icon-button>
</ha-md-list-item>`;
</paper-icon-item>`;
})}
${this._renderSpacer()}`
: ""}`;
@ -553,34 +557,41 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return html`<div class="spacer" disabled></div>`;
}
private _renderConfiguration(title: string | null, selectedPanel: string) {
return html`
<ha-md-list-item
class="configuration${selectedPanel === "config" ? " selected" : ""}"
type="button"
href="/config"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
private _renderConfiguration(title: string | null) {
return html`<a
class="configuration-container"
role="option"
aria-selected=${this.hass.panelUrl === "config"}
href="/config"
data-panel="config"
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item
class="configuration"
role="option"
aria-selected=${this.hass.panelUrl === "config"}
>
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
<ha-svg-icon slot="item-icon" .path=${mdiCog}></ha-svg-icon>
${!this.alwaysExpand &&
(this._updatesCount > 0 || this._issuesCount > 0)
? html`
<span class="badge" slot="start">
<span class="configuration-badge" slot="item-icon">
${this._updatesCount + this._issuesCount}
</span>
`
: ""}
<span class="item-text" slot="headline">${title}</span>
<span class="item-text">${title}</span>
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
? html`
<span class="badge" slot="end"
<span class="configuration-badge"
>${this._updatesCount + this._issuesCount}</span
>
`
: ""}
</ha-md-list-item>
`;
</paper-icon-item>
</a>`;
}
private _renderNotifications() {
@ -588,67 +599,91 @@ class HaSidebar extends SubscribeMixin(LitElement) {
? this._notifications.length
: 0;
return html`
<ha-md-list-item
return html`<div
class="notifications-container"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item
class="notifications"
role="option"
aria-selected="false"
@click=${this._handleShowNotificationDrawer}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
type="button"
>
<ha-svg-icon slot="start" .path=${mdiBell}></ha-svg-icon>
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
${!this.alwaysExpand && notificationCount > 0
? html`
<span class="badge" slot="start"> ${notificationCount} </span>
<span class="notification-badge" slot="item-icon">
${notificationCount}
</span>
`
: ""}
<span class="item-text" slot="headline"
>${this.hass.localize("ui.notification_drawer.title")}</span
>
<span class="item-text">
${this.hass.localize("ui.notification_drawer.title")}
</span>
${this.alwaysExpand && notificationCount > 0
? html`<span class="badge" slot="end">${notificationCount}</span>`
? html` <span class="notification-badge">${notificationCount}</span> `
: ""}
</ha-md-list-item>
`;
</paper-icon-item>
</div>`;
}
private _renderUserItem(selectedPanel: string) {
return html`
<ha-md-list-item
href="/profile"
type="link"
class="user ${selectedPanel === "profile" ? " selected" : ""}"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
private _renderUserItem() {
return html`<a
class=${classMap({
profile: true,
// Mimic behavior that paper-listbox provides
"iron-selected": this.hass.panelUrl === "profile",
})}
href="/profile"
data-panel="panel"
tabindex="-1"
role="option"
aria-selected=${this.hass.panelUrl === "profile"}
aria-label=${this.hass.localize("panel.profile")}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-user-badge
slot="start"
slot="item-icon"
.user=${this.hass.user}
.hass=${this.hass}
></ha-user-badge>
<span class="item-text" slot="headline"
>${this.hass.user ? this.hass.user.name : ""}</span
>
</ha-md-list-item>
`;
<span class="item-text">
${this.hass.user ? this.hass.user.name : ""}
</span>
</paper-icon-item>
</a>`;
}
private _renderExternalConfiguration() {
return html`${!this.hass.user?.is_admin &&
this.hass.auth.external?.config.hasSettingsScreen
? html`
<ha-md-list-item
<a
role="option"
aria-label=${this.hass.localize(
"ui.sidebar.external_app_configuration"
)}
href="#external-app-configuration"
tabindex="-1"
aria-selected="false"
@click=${this._handleExternalAppConfiguration}
type="button"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
<span class="item-text" slot="headline">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</ha-md-list-item>
<paper-icon-item>
<ha-svg-icon
slot="item-icon"
.path=${mdiCellphoneCog}
></ha-svg-icon>
<span class="item-text">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</paper-icon-item>
</a>
`
: ""}`;
}
@ -660,6 +695,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
});
}
private get _tooltip() {
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
}
private _handleAction(ev: CustomEvent<ActionHandlerDetail>) {
if (ev.detail.action !== "hold") {
return;
@ -722,7 +761,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
clearTimeout(this._mouseLeaveTimeout);
this._mouseLeaveTimeout = undefined;
}
this._showTooltip(ev.currentTarget as HaMdListItem);
this._showTooltip(ev.currentTarget as PaperIconItemElement);
}
private _itemMouseLeave() {
@ -735,10 +774,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
private _listboxFocusIn(ev) {
if (this.alwaysExpand || ev.target.localName !== "ha-md-list-item") {
if (this.alwaysExpand || ev.target.nodeName !== "A") {
return;
}
this._showTooltip(ev.target);
this._showTooltip(ev.target.querySelector("paper-icon-item"));
}
private _listboxFocusOut() {
@ -762,25 +801,22 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._recentKeydownActiveUntil = new Date().getTime() + 100;
}
private _showTooltip(item: HaMdListItem) {
private _showTooltip(item: PaperIconItemElement) {
if (this._tooltipHideTimeout) {
clearTimeout(this._tooltipHideTimeout);
this._tooltipHideTimeout = undefined;
}
const tooltip = this._tooltip;
const listbox = this.shadowRoot!.querySelector("ha-md-list")!;
const listbox = this.shadowRoot!.querySelector("paper-listbox")!;
let top = item.offsetTop + 11;
if (listbox.contains(item)) {
top += listbox.offsetTop;
top -= listbox.scrollTop;
}
tooltip.innerText = (
item.querySelector(".item-text") as HTMLElement
).innerText;
tooltip.innerHTML = item.querySelector(".item-text")!.innerHTML;
tooltip.style.display = "block";
tooltip.style.position = "fixed";
tooltip.style.top = `${top}px`;
tooltip.style.left = `${item.offsetLeft + item.clientWidth + 8}px`;
tooltip.style.left = `${item.offsetLeft + item.clientWidth + 4}px`;
}
private _hideTooltip() {
@ -869,11 +905,12 @@ class HaSidebar extends SubscribeMixin(LitElement) {
.menu mwc-button {
width: 100%;
}
.reorder-list,
.hidden-panel {
display: none;
}
ha-md-list {
paper-listbox {
padding: 4px 0;
display: flex;
flex-direction: column;
@ -885,64 +922,90 @@ class HaSidebar extends SubscribeMixin(LitElement) {
overflow-x: hidden;
background: none;
margin-left: env(safe-area-inset-left);
margin-inline-start: env(safe-area-inset-left);
margin-inline-end: initial;
}
ha-md-list-item {
a {
text-decoration: none;
color: var(--sidebar-text-color);
font-weight: 500;
font-size: 14px;
position: relative;
display: block;
outline: 0;
}
paper-icon-item {
box-sizing: border-box;
margin: 4px;
padding-left: 12px;
padding-inline-start: 12px;
padding-inline-end: initial;
border-radius: 4px;
height: 40px;
--md-list-item-one-line-container-height: 40px;
--paper-item-min-height: 40px;
width: 48px;
position: relative;
--md-list-item-label-text-color: var(--sidebar-text-color);
--md-list-item-leading-space: 12px;
--md-list-item-trailing-space: 12px;
--md-list-item-leading-icon-size: 24px;
}
:host([expanded]) ha-md-list-item {
:host([expanded]) paper-icon-item {
width: 248px;
width: calc(248px - env(safe-area-inset-left));
}
ha-md-list-item.selected {
--md-list-item-label-text-color: var(--sidebar-selected-icon-color);
--md-ripple-hover-color: var(--sidebar-selected-icon-color);
ha-icon[slot="item-icon"],
ha-svg-icon[slot="item-icon"] {
color: var(--sidebar-icon-color);
}
ha-md-list-item.selected::before {
.iron-selected paper-icon-item::before,
a:not(.iron-selected):focus::before {
border-radius: 4px;
position: absolute;
top: 0;
right: 0;
right: 2px;
bottom: 0;
left: 0;
left: 2px;
pointer-events: none;
content: "";
transition: opacity 15ms linear;
will-change: opacity;
}
.iron-selected paper-icon-item::before {
background-color: var(--sidebar-selected-icon-color);
opacity: 0.12;
}
a:not(.iron-selected):focus::before {
background-color: currentColor;
opacity: var(--dark-divider-opacity);
margin: 4px 8px;
}
.iron-selected paper-icon-item:focus::before,
.iron-selected:focus paper-icon-item::before {
opacity: 0.2;
}
ha-icon[slot="start"],
ha-svg-icon[slot="start"] {
width: 24px;
flex-shrink: 0;
color: var(--sidebar-icon-color);
.iron-selected paper-icon-item[pressed]:before {
opacity: 0.37;
}
ha-md-list-item.selected ha-svg-icon[slot="start"],
ha-md-list-item.selected ha-icon[slot="start"] {
color: var(--sidebar-selected-icon-color);
}
ha-md-list-item .item-text {
display: none;
max-width: calc(100% - 56px);
paper-icon-item span {
color: var(--sidebar-text-color);
font-weight: 500;
font-size: 14px;
}
:host([expanded]) ha-md-list-item .item-text {
a.iron-selected paper-icon-item ha-icon,
a.iron-selected paper-icon-item ha-svg-icon {
color: var(--sidebar-selected-icon-color);
}
a.iron-selected .item-text {
color: var(--sidebar-selected-text-color);
}
paper-icon-item .item-text {
display: none;
max-width: calc(100% - 56px);
}
:host([expanded]) paper-icon-item .item-text {
display: block;
}
@ -956,38 +1019,60 @@ class HaSidebar extends SubscribeMixin(LitElement) {
height: 1px;
background-color: var(--divider-color);
}
.badge {
.notifications-container,
.configuration-container {
display: flex;
justify-content: center;
align-items: center;
min-width: 8px;
border-radius: 10px;
margin-left: env(safe-area-inset-left);
margin-inline-start: env(safe-area-inset-left);
margin-inline-end: initial;
}
.notifications {
cursor: pointer;
}
.notifications .item-text,
.configuration .item-text {
flex: 1;
}
.profile {
margin-left: env(safe-area-inset-left);
margin-inline-start: env(safe-area-inset-left);
margin-inline-end: initial;
}
.profile paper-icon-item {
padding-left: 4px;
padding-inline-start: 4px;
padding-inline-end: auto;
}
.profile .item-text {
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
.notification-badge,
.configuration-badge {
position: absolute;
left: calc(var(--app-drawer-width, 248px) - 42px);
inset-inline-start: calc(var(--app-drawer-width, 248px) - 42px);
inset-inline-end: initial;
min-width: 20px;
box-sizing: border-box;
border-radius: 50%;
font-weight: 400;
line-height: normal;
background-color: var(--accent-color);
padding: 2px 6px;
line-height: 20px;
text-align: center;
padding: 0px 2px;
color: var(--text-accent-color, var(--text-primary-color));
}
ha-svg-icon + .badge {
ha-svg-icon + .notification-badge,
ha-svg-icon + .configuration-badge {
position: absolute;
top: 4px;
bottom: 14px;
left: 26px;
border-radius: 10px;
inset-inline-start: 26px;
inset-inline-end: initial;
font-size: 0.65em;
line-height: 2;
padding: 0 4px;
}
ha-md-list-item.user {
--md-list-item-leading-icon-size: 40px;
--md-list-item-bottom-space: 12px;
--md-list-item-leading-space: 4px;
--md-list-item-trailing-space: 4px;
}
ha-user-badge {
flex-shrink: 0;
}
.spacer {
@ -1003,6 +1088,19 @@ class HaSidebar extends SubscribeMixin(LitElement) {
white-space: nowrap;
}
.dev-tools {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0 8px;
width: 256px;
box-sizing: border-box;
}
.dev-tools a {
color: var(--sidebar-icon-color);
}
.tooltip {
display: none;
position: absolute;

115
src/components/ha-tabs.ts Normal file
View File

@ -0,0 +1,115 @@
import type { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
import type { PaperTabElement } from "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import type { PaperTabsElement } from "@polymer/paper-tabs/paper-tabs";
import { customElement } from "lit/decorators";
import type { Constructor } from "../types";
// eslint-disable-next-line @typescript-eslint/naming-convention
const PaperTabs = customElements.get(
"paper-tabs"
) as Constructor<PaperTabsElement>;
let subTemplate: HTMLTemplateElement;
@customElement("ha-tabs")
export class HaTabs extends PaperTabs {
private _firstTabWidth = 0;
private _lastTabWidth = 0;
private _lastLeftHiddenState = false;
private _lastRightHiddenState = false;
static get template(): HTMLTemplateElement {
if (!subTemplate) {
subTemplate = (PaperTabs as any).template.cloneNode(true);
const superStyle = subTemplate.content.querySelector("style");
// Add "noink" attribute for scroll buttons to disable animation.
subTemplate.content
.querySelectorAll("paper-icon-button")
.forEach((arrow: PaperIconButtonElement) => {
arrow.setAttribute("noink", "");
});
superStyle!.appendChild(
document.createTextNode(`
#selectionBar {
box-sizing: border-box;
}
.not-visible {
display: none;
}
paper-icon-button {
width: 24px;
height: 48px;
padding: 0;
margin: 0;
}
`)
);
}
return subTemplate;
}
// Get first and last tab's width for _affectScroll
// eslint-disable-next-line @typescript-eslint/naming-convention
public _tabChanged(tab: PaperTabElement, old: PaperTabElement): void {
super._tabChanged(tab, old);
const tabs = this.querySelectorAll("paper-tab:not(.hide-tab)");
if (tabs.length > 0) {
this._firstTabWidth = tabs[0].clientWidth;
this._lastTabWidth = tabs[tabs.length - 1].clientWidth;
}
// Scroll active tab into view if needed.
const selected = this.querySelector(".iron-selected");
if (selected) {
selected.scrollIntoView();
this._affectScroll(0); // Ensure scroll arrows match scroll position
}
}
/**
* Modify _affectScroll so that when the scroll arrows appear
* while scrolling and the tab container shrinks we can counteract
* the jump in tab position so that the scroll still appears smooth.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public _affectScroll(dx: number): void {
if (this._firstTabWidth === 0 || this._lastTabWidth === 0) {
return;
}
this.$.tabsContainer.scrollLeft += dx;
const scrollLeft = this.$.tabsContainer.scrollLeft;
const dirRTL = this.dir === "rtl";
const boolCondition1 = Math.abs(scrollLeft) < this._firstTabWidth;
const boolCondition2 =
Math.abs(scrollLeft) + this._lastTabWidth > this._tabContainerScrollSize;
this._leftHidden = !dirRTL ? boolCondition1 : boolCondition2;
this._rightHidden = !dirRTL ? boolCondition2 : boolCondition1;
if (!dirRTL) {
if (this._lastLeftHiddenState !== this._leftHidden) {
this._lastLeftHiddenState = this._leftHidden;
this.$.tabsContainer.scrollLeft += this._leftHidden ? -23 : 23;
}
} else if (this._lastRightHiddenState !== this._rightHidden) {
this._lastRightHiddenState = this._rightHidden;
this.$.tabsContainer.scrollLeft -= this._rightHidden ? -23 : 23;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-tabs": HaTabs;
}
}

View File

@ -136,12 +136,13 @@ export class HaTextField extends TextFieldBase {
}
.mdc-floating-label:not(.mdc-floating-label--float-above) {
max-width: calc(100% - 16px);
}
.mdc-floating-label--float-above {
max-width: calc((100% - 16px) / 0.75);
transition: none;
text-overflow: ellipsis;
width: inherit;
padding-right: 30px;
padding-inline-end: 30px;
padding-inline-start: initial;
box-sizing: border-box;
direction: var(--direction);
}
input {
@ -182,15 +183,11 @@ export class HaTextField extends TextFieldBase {
}
.mdc-floating-label {
padding-inline-end: 16px;
padding-inline-start: initial;
inset-inline-start: 16px !important;
inset-inline-end: initial !important;
transform-origin: var(--float-start);
direction: var(--direction);
text-align: var(--float-start);
box-sizing: border-box;
text-overflow: ellipsis;
}
.mdc-text-field--with-leading-icon.mdc-text-field--filled

View File

@ -1,9 +1,8 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import {
addWebRtcCandidate,
@ -27,10 +26,6 @@ class HaWebRtcPlayer extends LitElement {
@property() public entityid?: string;
@property({ attribute: false }) public aspectRatio?: number;
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
@property({ type: Boolean, attribute: "controls" })
public controls = false;
@ -74,11 +69,6 @@ class HaWebRtcPlayer extends LitElement {
?controls=${this.controls}
poster=${ifDefined(this.posterUrl)}
@loadeddata=${this._loadedData}
style=${styleMap({
height: this.aspectRatio == null ? "100%" : "auto",
aspectRatio: this.aspectRatio,
objectFit: this.fitMode,
})}
></video>
`;
}

View File

@ -1,160 +0,0 @@
import TabGroup from "@shoelace-style/shoelace/dist/components/tab-group/tab-group.component";
import TabGroupStyles from "@shoelace-style/shoelace/dist/components/tab-group/tab-group.styles";
import "@shoelace-style/shoelace/dist/components/tab/tab";
import type { PropertyValues } from "lit";
import { css } from "lit";
import { customElement, query } from "lit/decorators";
@customElement("sl-tab-group")
// @ts-ignore
export class HaSlTabGroup extends TabGroup {
private _mouseIsDown = false;
private _scrolled = false;
private _mouseReleasedAt?: number;
private _scrollStartX = 0;
private _scrollLeft = 0;
@query(".tab-group__nav", true) private _scrollContainer?: HTMLElement;
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener("mousemove", this._mouseMove);
window.removeEventListener("mouseup", this._mouseUp);
}
override setAriaLabels() {
// Override the method to prevent setting aria-labels, as we don't use panels
// and don't want to set aria-labels for the tabs
}
override getAllPanels() {
// Override the method to prevent querying for panels
// and return an empty array instead
// as we don't use panels
return [];
}
protected override firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
const scrollContainer = this._scrollContainer;
if (scrollContainer) {
scrollContainer.addEventListener("mousedown", this._mouseDown);
}
}
// @ts-ignore
protected override handleClick(event: MouseEvent) {
if (
this._mouseReleasedAt &&
new Date().getTime() - this._mouseReleasedAt < 100
) {
return;
}
// @ts-ignore
super.handleClick(event);
}
private _mouseDown = (event: MouseEvent) => {
const scrollContainer = this._scrollContainer;
if (!scrollContainer) {
return;
}
this._scrollStartX = event.pageX - scrollContainer.offsetLeft;
this._scrollLeft = scrollContainer.scrollLeft;
this._mouseIsDown = true;
this._scrolled = false;
window.addEventListener("mousemove", this._mouseMove);
window.addEventListener("mouseup", this._mouseUp, { once: true });
};
private _mouseUp = () => {
this._mouseIsDown = false;
if (this._scrolled) {
this._mouseReleasedAt = new Date().getTime();
}
window.removeEventListener("mousemove", this._mouseMove);
};
private _mouseMove = (event: MouseEvent) => {
if (!this._mouseIsDown) {
return;
}
const scrollContainer = this._scrollContainer;
if (!scrollContainer) {
return;
}
const x = event.pageX - scrollContainer.offsetLeft;
const scroll = x - this._scrollStartX;
if (!this._scrolled) {
this._scrolled = Math.abs(scroll) > 1;
}
scrollContainer.scrollLeft = this._scrollLeft - scroll;
};
static override styles = [
TabGroupStyles,
css`
:host {
--sl-spacing-3x-small: 0.125rem;
--sl-spacing-2x-small: 0.25rem;
--sl-spacing-x-small: 0.5rem;
--sl-spacing-small: 0.75rem;
--sl-spacing-medium: 1rem;
--sl-spacing-large: 1.25rem;
--sl-spacing-x-large: 1.75rem;
--sl-spacing-2x-large: 2.25rem;
--sl-spacing-3x-large: 3rem;
--sl-spacing-4x-large: 4.5rem;
--sl-transition-x-slow: 1000ms;
--sl-transition-slow: 500ms;
--sl-transition-medium: 250ms;
--sl-transition-fast: 150ms;
--sl-transition-x-fast: 50ms;
--transition-speed: var(--sl-transition-fast);
--sl-border-radius-small: 0.1875rem;
--sl-border-radius-medium: 0.25rem;
--sl-border-radius-large: 0.5rem;
--sl-border-radius-x-large: 1rem;
--sl-border-radius-circle: 50%;
--sl-border-radius-pill: 9999px;
--sl-color-neutral-600: inherit;
--sl-font-weight-semibold: 500;
--sl-font-size-small: 14px;
--sl-color-primary-600: var(
--ha-tab-active-text-color,
var(--primary-color)
);
--track-color: var(--ha-tab-track-color, var(--divider-color));
--indicator-color: var(--ha-tab-indicator-color, var(--primary-color));
}
::slotted(sl-tab:not([active])) {
opacity: 0.8;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
// @ts-ignore
"sl-tab-group": HaSlTabGroup;
}
}

View File

@ -19,16 +19,9 @@ import "../../panels/logbook/ha-logbook-renderer";
import { traceTabStyles } from "./trace-tab-styles";
import type { HomeAssistant } from "../../types";
import type { NodeInfo } from "./hat-script-graph";
import { describeCondition, describeTrigger } from "../../data/automation_i18n";
import { describeCondition } from "../../data/automation_i18n";
import type { EntityRegistryEntry } from "../../data/entity_registry";
import type { LabelRegistryEntry } from "../../data/label_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import { describeAction } from "../../data/script_i18n";
import { fullEntitiesContext } from "../../data/context";
const TRACE_PATH_TABS = [
"step_config",
@ -59,14 +52,6 @@ export class HaTracePathDetails extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state()
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: Record<string, FloorRegistryEntry>;
protected render(): TemplateResult {
return html`
<div class="padded-box trace-info">
@ -166,46 +151,11 @@ export class HaTracePathDetails extends LitElement {
)}`;
}
const selectedType = this.selected.type;
return html`
${curPath === this.selected.path
? currentDetail.alias
? html`<h2>${currentDetail.alias}</h2>`
: selectedType === "trigger"
? html`<h2>
${describeTrigger(
currentDetail,
this.hass,
this._entityReg
)}
</h2>`
: selectedType === "condition"
? html`<h2>
${describeCondition(
currentDetail,
this.hass,
this._entityReg
)}
</h2>`
: selectedType === "action"
? html`<h2>
${describeAction(
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
currentDetail
)}
</h2>`
: selectedType === "chooseOption"
? html`<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option",
{ number: pathParts[pathParts.length - 1] }
)}
</h2>`
: nothing
: nothing
: html`<h2>
${curPath.substring(this.selected.path.length + 1)}
</h2>`}

View File

@ -53,12 +53,9 @@ import "./hat-graph-node";
import "./hat-graph-spacer";
import { ACTION_ICONS } from "../../data/action";
type NodeType = "trigger" | "condition" | "action" | "chooseOption" | undefined;
export interface NodeInfo {
path: string;
config: any;
type?: NodeType;
}
declare global {
@ -79,16 +76,16 @@ export class HatScriptGraph extends LitElement {
public trackedNodes: Record<string, NodeInfo> = {};
private _selectNode(config, path, type?) {
private _selectNode(config, path) {
return () => {
fireEvent(this, "graph-node-selected", { config, path, type });
fireEvent(this, "graph-node-selected", { config, path });
};
}
private _renderTrigger(config: Trigger, i: number) {
const path = `trigger/${i}`;
const track = this.trace && path in this.trace.trace;
this.renderedNodes[path] = { config, path, type: "trigger" };
this.renderedNodes[path] = { config, path };
if (track) {
this.trackedNodes[path] = this.renderedNodes[path];
}
@ -96,7 +93,7 @@ export class HatScriptGraph extends LitElement {
<hat-graph-node
graph-start
?track=${track}
@focus=${this._selectNode(config, path, "trigger")}
@focus=${this._selectNode(config, path)}
?active=${this.selected === path}
.iconPath=${mdiAsterisk}
.notEnabled=${"enabled" in config && config.enabled === false}
@ -108,7 +105,7 @@ export class HatScriptGraph extends LitElement {
private _renderCondition(config: Condition, i: number) {
const path = `condition/${i}`;
this.renderedNodes[path] = { config, path, type: "condition" };
this.renderedNodes[path] = { config, path };
if (this.trace && path in this.trace.trace) {
this.trackedNodes[path] = this.renderedNodes[path];
}
@ -139,7 +136,7 @@ export class HatScriptGraph extends LitElement {
) {
const type =
Object.keys(this._typeRenderers).find((key) => key in node) || "other";
this.renderedNodes[path] = { config: node, path, type: "action" };
this.renderedNodes[path] = { config: node, path };
if (this.trace && path in this.trace.trace) {
this.trackedNodes[path] = this.renderedNodes[path];
}
@ -169,7 +166,7 @@ export class HatScriptGraph extends LitElement {
return html`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this._selectNode(config, path, "action")}
@focus=${this._selectNode(config, path)}
?track=${trace !== undefined}
?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
@ -189,11 +186,7 @@ export class HatScriptGraph extends LitElement {
? ensureArray(config.choose)?.map((branch, i) => {
const branchPath = `${path}/choose/${i}`;
const trackThis = tracePath.includes(i);
this.renderedNodes[branchPath] = {
config: branch,
path: branchPath,
type: "chooseOption",
};
this.renderedNodes[branchPath] = { config, path: branchPath };
if (trackThis) {
this.trackedNodes[branchPath] = this.renderedNodes[branchPath];
}
@ -203,11 +196,7 @@ export class HatScriptGraph extends LitElement {
.iconPath=${!trace || trackThis
? mdiCheckboxMarkedOutline
: mdiCheckboxBlankOutline}
@focus=${this._selectNode(
branch,
branchPath,
"chooseOption"
)}
@focus=${this._selectNode(config, branchPath)}
?track=${trackThis}
?active=${this.selected === branchPath}
.notEnabled=${disabled || config.enabled === false}
@ -267,7 +256,7 @@ export class HatScriptGraph extends LitElement {
return html`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this._selectNode(config, path, "action")}
@focus=${this._selectNode(config, path)}
?track=${trace !== undefined}
?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
@ -348,7 +337,7 @@ export class HatScriptGraph extends LitElement {
}
return html`
<hat-graph-branch
@focus=${this._selectNode(node, path, "condition")}
@focus=${this._selectNode(node, path)}
?track=${track}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
@ -392,7 +381,7 @@ export class HatScriptGraph extends LitElement {
return html`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this._selectNode(node, path, "action")}
@focus=${this._selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
@ -438,7 +427,7 @@ export class HatScriptGraph extends LitElement {
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${node.action ? undefined : mdiRoomService}
@focus=${this._selectNode(node, path, "action")}
@focus=${this._selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
@ -466,7 +455,7 @@ export class HatScriptGraph extends LitElement {
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiCodeBraces}
@focus=${this._selectNode(node, path, "action")}
@focus=${this._selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
@ -486,7 +475,7 @@ export class HatScriptGraph extends LitElement {
return html`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this._selectNode(node, path, "action")}
@focus=${this._selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
@ -524,7 +513,7 @@ export class HatScriptGraph extends LitElement {
return html`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this._selectNode(node, path, "action")}
@focus=${this._selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
@ -573,7 +562,7 @@ export class HatScriptGraph extends LitElement {
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${ACTION_ICONS[getActionType(node)] || mdiCodeBrackets}
@focus=${this._selectNode(node, path, "action")}
@focus=${this._selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}

View File

@ -84,24 +84,21 @@ class UserBadge extends LitElement {
static styles = css`
:host {
display: block;
width: 40px;
height: 40px;
display: contents;
}
.picture {
width: 100%;
height: 100%;
width: 40px;
height: 40px;
background-size: cover;
border-radius: 50%;
}
.initials {
display: inline-flex;
justify-content: center;
align-items: center;
display: inline-block;
box-sizing: border-box;
width: 100%;
height: 100%;
width: 40px;
line-height: 40px;
border-radius: 50%;
text-align: center;
background-color: var(--light-primary-color);
text-decoration: none;
color: var(--text-light-primary-color, var(--primary-text-color));

View File

@ -52,8 +52,7 @@ export const assistSatelliteAnnounce = (
args: {
message?: string;
media_id?: string;
preannounce?: boolean;
preannounce_media_id?: string;
preannounce_media_id?: string | null;
}
) => hass.callService("assist_satellite", "announce", args, { entity_id });

View File

@ -66,16 +66,11 @@ export type ManagerStateEvent =
export const subscribeBackupEvents = (
hass: HomeAssistant,
callback: (event: ManagerStateEvent) => void,
preCheck?: () => boolean | Promise<boolean>
callback: (event: ManagerStateEvent) => void
) =>
hass.connection.subscribeMessage<ManagerStateEvent>(
callback,
{
type: "backup/subscribe_events",
},
{ preCheck }
);
hass.connection.subscribeMessage<ManagerStateEvent>(callback, {
type: "backup/subscribe_events",
});
export const DEFAULT_MANAGER_STATE: ManagerStateEvent = {
manager_state: "idle",

View File

@ -1,5 +1,7 @@
import type { Connection } from "home-assistant-js-websocket";
import { getCollection } from "home-assistant-js-websocket";
import type { LocalizeFunc } from "../common/translations/localize";
import { debounce } from "../common/util/debounce";
import type { HomeAssistant } from "../types";
import type {
DataEntryFlowProgress,
@ -91,20 +93,31 @@ export const fetchConfigFlowInProgress = (
type: "config_entries/flow/progress",
});
export interface ConfigFlowInProgressMessage {
type: null | "added" | "removed";
flow_id: string;
flow: DataEntryFlowProgress;
}
const subscribeConfigFlowInProgressUpdates = (conn: Connection, store) =>
conn.subscribeEvents(
debounce(
() =>
fetchConfigFlowInProgress(conn).then((flows: DataEntryFlowProgress[]) =>
store.setState(flows, true)
),
500,
true
),
"config_entry_discovered"
);
export const getConfigFlowInProgressCollection = (conn: Connection) =>
getCollection<DataEntryFlowProgress[]>(
conn,
"_configFlowProgress",
fetchConfigFlowInProgress,
subscribeConfigFlowInProgressUpdates
);
export const subscribeConfigFlowInProgress = (
hass: HomeAssistant,
onChange: (update: ConfigFlowInProgressMessage[]) => void
) =>
hass.connection.subscribeMessage<ConfigFlowInProgressMessage[]>(
(message) => onChange(message),
{ type: "config_entries/flow/subscribe" }
);
onChange: (flows: DataEntryFlowProgress[]) => void
) => getConfigFlowInProgressCollection(hass.connection).subscribe(onChange);
export const localizeConfigFlowTitle = (
localize: LocalizeFunc,

View File

@ -17,15 +17,6 @@ export interface DataEntryFlowProgressedEvent {
};
}
export interface DataEntryFlowProgressEvent {
type: "data_entry_flow_progress_update";
data: {
handler: string;
flow_id: string;
progress: number;
};
}
export interface DataEntryFlowProgress {
flow_id: string;
handler: string;
@ -117,12 +108,3 @@ export const subscribeDataEntryFlowProgressed = (
callback,
"data_entry_flow_progressed"
);
export const subscribeDataEntryFlowProgress = (
conn: Connection,
callback: (ev: DataEntryFlowProgressEvent) => void
) =>
conn.subscribeEvents<DataEntryFlowProgressEvent>(
callback,
"data_entry_flow_progress_update"
);

View File

@ -3,7 +3,6 @@ import { getOptimisticCollection } from "./collection";
export interface CoreFrontendUserData {
showAdvanced?: boolean;
showEntityIdPicker?: boolean;
}
declare global {

View File

@ -154,9 +154,3 @@ export const subscribeLogInfo = (
conn,
onChange
);
export const waitForIntegrationSetup = (hass: HomeAssistant, domain: string) =>
hass.callWS<{ integration_loaded: boolean }>({
type: "integration/wait",
domain,
});

View File

@ -128,11 +128,3 @@ export const forgotPasswordHaCloud = async (email: string) =>
body: JSON.stringify({ email }),
})
);
export const waitForIntegration = (domain: string) =>
handleFetchPromise<{ integration_loaded: boolean }>(
fetch("/api/onboarding/integration/wait", {
method: "POST",
body: JSON.stringify({ domain }),
})
);

View File

@ -94,14 +94,7 @@ const tryDescribeAction = <T extends ActionType>(
const targets: string[] = [];
const targetOrData = config.target || config.data;
if (typeof targetOrData === "string" && isTemplate(targetOrData)) {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_template`,
{ name: "target" }
)
);
} else if (targetOrData) {
if (targetOrData) {
for (const [key, name] of Object.entries({
area_id: "areas",
device_id: "devices",

View File

@ -1,21 +0,0 @@
import type { HomeAssistant } from "../../types";
export interface SupervisorUpdateConfig {
add_on_backup_before_update: boolean;
add_on_backup_retain_copies?: number;
core_backup_before_update: boolean;
}
export const getSupervisorUpdateConfig = async (hass: HomeAssistant) =>
hass.callWS<SupervisorUpdateConfig>({
type: "hassio/update/config/info",
});
export const updateSupervisorUpdateConfig = async (
hass: HomeAssistant,
config: Partial<SupervisorUpdateConfig>
) =>
hass.callWS({
type: "hassio/update/config/update",
...config,
});

View File

@ -207,11 +207,7 @@ export const computeUpdateStateDisplay = (
return hass.formatEntityState(stateObj);
};
export type UpdateType =
| "addon"
| "home_assistant"
| "home_assistant_os"
| "generic";
type UpdateType = "addon" | "home_assistant" | "generic";
export const getUpdateType = (
stateObj: UpdateEntity,
@ -219,7 +215,6 @@ export const getUpdateType = (
): UpdateType => {
const entity_id = stateObj.entity_id;
const domain = entitySources[entity_id]?.domain;
if (domain !== "hassio") {
return "generic";
}
@ -229,11 +224,13 @@ export const getUpdateType = (
return "home_assistant";
}
if (title === HOME_ASSISTANT_OS_TITLE) {
return "home_assistant_os";
}
if (title !== HOME_ASSISTANT_SUPERVISOR_TITLE) {
if (
![
HOME_ASSISTANT_CORE_TITLE,
HOME_ASSISTANT_SUPERVISOR_TITLE,
HOME_ASSISTANT_OS_TITLE,
].includes(title)
) {
return "addon";
}
return "generic";

View File

@ -80,7 +80,7 @@ enum QRCodeVersion {
SmartStart = 1,
}
export enum Protocols {
enum Protocols {
ZWave = 0,
ZWaveLongRange = 1,
}
@ -151,35 +151,12 @@ export interface QRProvisioningInformation {
maxInclusionRequestInterval?: number | undefined;
uuid?: string | undefined;
supportedProtocols?: Protocols[] | undefined;
status?: ProvisioningEntryStatus;
}
export interface PlannedProvisioningEntry {
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
dsk: string;
securityClasses: SecurityClass[];
status?: ProvisioningEntryStatus;
}
export enum ProvisioningEntryStatus {
Active = 0,
Inactive = 1,
}
export interface DeviceConfig {
filename: string;
manufacturer: string;
manufacturerId: number;
label: string;
description: string;
devices: {
productType: number;
productId: number;
}[];
firmwareVersion: {
min: string;
max: string;
};
}
export const MINIMUM_QR_STRING_LENGTH = 52;
@ -218,7 +195,6 @@ export interface ZWaveJSController {
is_rebuilding_routes: boolean;
inclusion_state: InclusionState;
nodes: ZWaveJSNodeStatus[];
supports_long_range: boolean;
}
export interface ZWaveJSNodeStatus {
@ -579,7 +555,7 @@ export const zwaveTryParseDskFromQrCode = (
export const zwaveValidateDskAndEnterPin = (
hass: HomeAssistant,
entry_id: string,
pin: string | false
pin: string
) =>
hass.callWS({
type: "zwave_js/validate_dsk_and_enter_pin",
@ -609,38 +585,19 @@ export const zwaveParseQrCode = (
qr_code_string,
});
export const lookupZwaveDevice = (
hass: HomeAssistant,
entry_id: string,
manufacturerId: number,
productType: number,
productId: number,
applicationVersion?: string
): Promise<DeviceConfig> =>
hass.callWS({
type: "zwave_js/lookup_device",
entry_id,
manufacturerId,
productType,
productId,
applicationVersion,
});
export const provisionZwaveSmartStartNode = (
hass: HomeAssistant,
entry_id: string,
qr_provisioning_information?: QRProvisioningInformation,
protocol?: Protocols,
device_name?: string,
area_id?: string
): Promise<string> =>
qr_code_string?: string,
planned_provisioning_entry?: PlannedProvisioningEntry
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/provision_smart_start_node",
entry_id,
qr_code_string,
qr_provisioning_information,
protocol,
device_name,
area_id,
planned_provisioning_entry,
});
export const unprovisionZwaveSmartStartNode = (
@ -656,16 +613,6 @@ export const unprovisionZwaveSmartStartNode = (
node_id,
});
export const subscribeNewDevices = (
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: any) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage((message) => callbackFunction(message), {
type: "zwave_js/subscribe_new_devices",
entry_id: entry_id,
});
export const fetchZwaveNodeStatus = (
hass: HomeAssistant,
device_id: string

View File

@ -9,10 +9,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog";
import "../../components/ha-icon-button";
import type { DataEntryFlowStep } from "../../data/data_entry_flow";
import {
subscribeDataEntryFlowProgress,
subscribeDataEntryFlowProgressed,
} from "../../data/data_entry_flow";
import { subscribeDataEntryFlowProgressed } from "../../data/data_entry_flow";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
@ -55,8 +52,6 @@ class DataEntryFlowDialog extends LitElement {
@state() private _loading?: LoadingReason;
@state() private _progress?: number;
private _instance = instance;
@state() private _step:
@ -67,7 +62,7 @@ class DataEntryFlowDialog extends LitElement {
@state() private _handler?: string;
private _unsubDataEntryFlowProgress?: UnsubscribeFunc;
private _unsubDataEntryFlowProgressed?: Promise<UnsubscribeFunc>;
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
this._params = params;
@ -165,9 +160,11 @@ class DataEntryFlowDialog extends LitElement {
this._step = undefined;
this._params = undefined;
this._handler = undefined;
if (this._unsubDataEntryFlowProgress) {
this._unsubDataEntryFlowProgress();
this._unsubDataEntryFlowProgress = undefined;
if (this._unsubDataEntryFlowProgressed) {
this._unsubDataEntryFlowProgressed.then((unsub) => {
unsub();
});
this._unsubDataEntryFlowProgressed = undefined;
}
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@ -258,9 +255,7 @@ class DataEntryFlowDialog extends LitElement {
.params=${this._params}
.step=${this._step}
.hass=${this.hass}
.handler=${this._step.handler}
.domain=${this._params.domain ??
this._step.handler}
.domain=${this._step.handler}
></step-flow-abort>
`
: this._step.type === "progress"
@ -269,7 +264,6 @@ class DataEntryFlowDialog extends LitElement {
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.progress=${this._progress}
></step-flow-progress>
`
: this._step.type === "menu"
@ -345,28 +339,20 @@ class DataEntryFlowDialog extends LitElement {
}
private async _subscribeDataEntryFlowProgressed() {
if (this._unsubDataEntryFlowProgress) {
if (this._unsubDataEntryFlowProgressed) {
return;
}
this._progress = undefined;
const unsubs = [
subscribeDataEntryFlowProgressed(this.hass.connection, (ev) => {
this._unsubDataEntryFlowProgressed = subscribeDataEntryFlowProgressed(
this.hass.connection,
async (ev) => {
if (ev.data.flow_id !== this._step?.flow_id) {
return;
}
this._processStep(
this._params!.flowConfig.fetchFlow(this.hass, this._step.flow_id)
);
this._progress = undefined;
}),
subscribeDataEntryFlowProgress(this.hass.connection, (ev) => {
// ha-progress-ring has an issue with 0 so we round up
this._progress = Math.ceil(ev.data.progress * 100);
}),
];
this._unsubDataEntryFlowProgress = async () => {
(await Promise.all(unsubs)).map((unsub) => unsub());
};
}
);
}
static get styles(): CSSResultGroup {

View File

@ -20,8 +20,6 @@ class StepFlowAbort extends LitElement {
@property({ attribute: false }) public domain!: string;
@property({ attribute: false }) public handler!: string;
protected firstUpdated(changed: PropertyValues) {
super.firstUpdated(changed);
if (this.step.reason === "missing_credentials") {
@ -60,7 +58,7 @@ class StepFlowAbort extends LitElement {
applicationCredentialAddedCallback: () => {
showConfigFlowDialog(this.params.dialogParentElement!, {
dialogClosedCallback: this.params.dialogClosedCallback,
startFlowHandler: this.handler,
startFlowHandler: this.domain,
showAdvanced: this.hass.userData?.showAdvanced,
navigateToResult: this.params.navigateToResult,
});

View File

@ -84,7 +84,7 @@ class StepFlowForm extends LitElement {
${this._loading
? html`
<div class="submit-spinner">
<ha-spinner size="small"></ha-spinner>
<ha-spinner></ha-spinner>
</div>
`
: html`
@ -263,9 +263,6 @@ class StepFlowForm extends LitElement {
}
.submit-spinner {
height: 36px;
display: flex;
align-items: center;
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;

View File

@ -2,13 +2,11 @@ import "@material/mwc-button";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../components/ha-progress-ring";
import "../../components/ha-spinner";
import type { DataEntryFlowStepProgress } from "../../data/data_entry_flow";
import type { HomeAssistant } from "../../types";
import type { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
import { blankBeforePercent } from "../../common/translations/blank_before_percent";
@customElement("step-flow-progress")
class StepFlowProgress extends LitElement {
@ -21,24 +19,13 @@ class StepFlowProgress extends LitElement {
@property({ attribute: false })
public step!: DataEntryFlowStepProgress;
@property({ type: Number })
public progress?: number;
protected render(): TemplateResult {
return html`
<h2>
${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)}
</h2>
<div class="content">
${this.progress
? html`
<ha-progress-ring .value=${this.progress} size="large"
>${this.progress}${blankBeforePercent(
this.hass.locale
)}%</ha-progress-ring
>
`
: html` <ha-spinner size="large"></ha-spinner> `}
<ha-spinner></ha-spinner>
${this.flowConfig.renderShowFormProgressDescription(
this.hass,
this.step

View File

@ -45,8 +45,7 @@ class MoreInfoCamera extends LitElement {
<ha-progress-button
@click=${this._downloadSnapshot}
.progress=${this._waiting}
.disabled=${this.stateObj.state === UNAVAILABLE ||
this.stateObj.state === "idle"}
.disabled=${this.stateObj.state === UNAVAILABLE}
>
${this.hass.localize(
"ui.dialogs.more_info_control.camera.download_snapshot"

View File

@ -1,27 +1,26 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { BINARY_STATE_OFF } from "../../../common/const";
import { relativeTime } from "../../../common/datetime/relative_time";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-checkbox";
import "../../../components/ha-spinner";
import "../../../components/ha-faded";
import "../../../components/ha-formfield";
import "../../../components/ha-markdown";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import type { BackupConfig } from "../../../data/backup";
import { fetchBackupConfig } from "../../../data/backup";
import { isUnavailableState } from "../../../data/entity";
import type { EntitySources } from "../../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../../data/entity_sources";
import { getSupervisorUpdateConfig } from "../../../data/supervisor/update";
import type { UpdateEntity, UpdateType } from "../../../data/update";
import type { UpdateEntity } from "../../../data/update";
import {
getUpdateType,
UpdateEntityFeature,
@ -45,49 +44,17 @@ class MoreInfoUpdate extends LitElement {
@state() private _backupConfig?: BackupConfig;
@state() private _createBackup = false;
@state() private _entitySources?: EntitySources;
private async _fetchBackupConfig() {
try {
const { config } = await fetchBackupConfig(this.hass);
this._backupConfig = config;
} catch (err) {
// ignore error, because user will get a manual backup option
// eslint-disable-next-line no-console
console.error(err);
}
}
private async _fetchUpdateBackupConfig(type: UpdateType) {
try {
const config = await getSupervisorUpdateConfig(this.hass);
// for home assistant and OS updates
if (this._isHaOrOsUpdate(type)) {
this._createBackup = config.core_backup_before_update;
return;
}
if (type === "addon") {
this._createBackup = config.add_on_backup_before_update;
}
} catch (err) {
// ignore error, because user can still set the config
// eslint-disable-next-line no-console
console.error(err);
}
const { config } = await fetchBackupConfig(this.hass);
this._backupConfig = config;
}
private async _fetchEntitySources() {
this._entitySources = await fetchEntitySourcesWithCache(this.hass);
}
private _isHaOrOsUpdate(type: UpdateType): boolean {
return ["home_assistant", "home_assistant_os"].includes(type);
}
private _computeCreateBackupTexts():
| { title: string; description?: string }
| undefined {
@ -102,7 +69,8 @@ class MoreInfoUpdate extends LitElement {
? getUpdateType(this.stateObj, this._entitySources)
: "generic";
if (this._isHaOrOsUpdate(updateType)) {
// Automatic or manual for Home Assistant update
if (updateType === "home_assistant") {
const isBackupConfigValid =
!!this._backupConfig &&
!!this._backupConfig.automatic_backups_configured &&
@ -288,8 +256,7 @@ class MoreInfoUpdate extends LitElement {
: nothing}
<ha-switch
slot="end"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
id="create-backup"
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-md-list-item>
@ -352,14 +319,7 @@ class MoreInfoUpdate extends LitElement {
if (supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
this._fetchEntitySources().then(() => {
const type = getUpdateType(this.stateObj!, this._entitySources!);
if (
isComponentLoaded(this.hass, "hassio") &&
["addon", "home_assistant", "home_assistant_os"].includes(type)
) {
this._fetchUpdateBackupConfig(type);
}
if (this._isHaOrOsUpdate(type)) {
if (type === "home_assistant") {
this._fetchBackupConfig();
}
});
@ -387,7 +347,13 @@ class MoreInfoUpdate extends LitElement {
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
return false;
}
return this._createBackup;
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
}
return false;
}
private _handleInstall(): void {
@ -409,10 +375,6 @@ class MoreInfoUpdate extends LitElement {
this.hass.callService("update", "install", installData);
}
private _createBackupChanged(ev) {
this._createBackup = ev.target.checked;
}
private _handleSkip(): void {
if (this.stateObj!.attributes.auto_update) {
showAlertDialog(this, {

View File

@ -694,14 +694,9 @@ export class MoreInfoDialog extends LitElement {
background: none;
border: none;
outline: none;
display: inline;
display: inline-flex;
border-radius: 6px;
transition: background-color 180ms ease-in-out;
min-width: 0;
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
text-align: left;
}
.title button.breadcrumb {

View File

@ -20,7 +20,6 @@ import type {
import { fetchStatistics, getStatisticMetadata } from "../../data/recorder";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import type { HomeAssistant } from "../../types";
import { haStyle } from "../../resources/styles";
declare global {
interface HASSDomEvents {
@ -59,9 +58,9 @@ export class MoreInfoHistory extends LitElement {
return html`${isComponentLoaded(this.hass, "history")
? html`<div class="header">
<h2>
<div class="title">
${this.hass.localize("ui.dialogs.more_info_control.history")}
</h2>
</div>
${__DEMO__
? nothing
: html`<a href=${this._showMoreHref}
@ -232,25 +231,27 @@ export class MoreInfoHistory extends LitElement {
this._setRedrawTimer();
}
static styles = [
haStyle,
css`
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.header > a,
a:visited {
color: var(--primary-color);
}
h2 {
margin: 0;
}
`,
];
static styles = css`
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.header > a,
a:visited {
color: var(--primary-color);
}
.title {
font-family: var(--paper-font-title_-_font-family);
-webkit-font-smoothing: var(--paper-font-title_-_-webkit-font-smoothing);
font-size: var(--paper-font-subhead_-_font-size);
font-weight: var(--paper-font-title_-_font-weight);
letter-spacing: var(--paper-font-title_-_letter-spacing);
line-height: var(--paper-font-title_-_line-height);
}
`;
}
declare global {

View File

@ -133,8 +133,8 @@ export class MoreInfoInfo extends LitElement {
[data-domain="camera"] .content {
padding: 0;
/* max height of the video is full screen, minus the height of the header of the dialog (79px) and the max height of the dialog (mdc-dialog-max-height: calc(100% - 72px)) and the actions bar 60px */
--video-max-height: calc(100vh - 72px - 79px - 60px);
/* max height of the video is full screen, minus the height of the header of the dialog and the padding of the dialog (mdc-dialog-max-height: calc(100% - 72px)) */
--video-max-height: calc(100vh - 65px - 72px);
}
more-info-content {

View File

@ -7,7 +7,6 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { createSearchParam } from "../../common/url/search-params";
import "../../panels/logbook/ha-logbook";
import type { HomeAssistant } from "../../types";
import { haStyle } from "../../resources/styles";
@customElement("ha-more-info-logbook")
export class MoreInfoLogbook extends LitElement {
@ -33,7 +32,9 @@ export class MoreInfoLogbook extends LitElement {
return html`
<div class="header">
<h2>${this.hass.localize("ui.dialogs.more_info_control.logbook")}</h2>
<div class="title">
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
</div>
<a href=${this._showMoreHref}
>${this.hass.localize("ui.dialogs.more_info_control.show_more")}</a
>
@ -67,7 +68,6 @@ export class MoreInfoLogbook extends LitElement {
static get styles() {
return [
haStyle,
css`
ha-logbook {
--logbook-max-height: 250px;
@ -88,8 +88,15 @@ export class MoreInfoLogbook extends LitElement {
a:visited {
color: var(--primary-color);
}
h2 {
margin: 0;
.title {
font-family: var(--paper-font-title_-_font-family);
-webkit-font-smoothing: var(
--paper-font-title_-_-webkit-font-smoothing
);
font-size: var(--paper-font-subhead_-_font-size);
font-weight: var(--paper-font-title_-_font-weight);
letter-spacing: var(--paper-font-title_-_letter-spacing);
line-height: var(--paper-font-title_-_line-height);
}
`,
];

View File

@ -25,14 +25,15 @@ export class HuiNotificationItemTemplate extends LitElement {
}
ha-card .header {
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
);
font-size: var(--paper-font-headline_-_font-size);
font-weight: var(--paper-font-headline_-_font-weight);
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
/* start paper-font-headline style */
font-family: "Roboto", "Noto", sans-serif;
-webkit-font-smoothing: antialiased; /* OS X subpixel AA bleed bug */
text-rendering: optimizeLegibility;
font-size: 24px;
font-weight: 400;
letter-spacing: -0.012em;
line-height: 32px;
/* end paper-font-headline style */
color: var(--primary-text-color);
padding: 16px 16px 0;

View File

@ -5,7 +5,6 @@ import {
mdiConsoleLine,
mdiDevices,
mdiEarth,
mdiKeyboard,
mdiMagnify,
mdiReload,
mdiServerNetwork,
@ -32,7 +31,6 @@ import "../../components/ha-label";
import "../../components/ha-list-item";
import "../../components/ha-spinner";
import "../../components/ha-textfield";
import "../../components/ha-tip";
import { fetchHassioAddonsInfo } from "../../data/hassio/addon";
import { domainToName } from "../../data/integration";
import { getPanelNameTranslationKey } from "../../data/panel";
@ -42,7 +40,6 @@ import { haStyleDialog, haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
interface QuickBarItem extends ScorableTextItem {
@ -425,12 +422,10 @@ export class QuickBar extends LitElement {
}
private _addSpinnerToCommandItem(index: number): void {
const div = document.createElement("div");
div.slot = "meta";
const spinner = document.createElement("ha-spinner");
spinner.size = "small";
div.appendChild(spinner);
this._getItemAtIndex(index)?.appendChild(div);
spinner.slot = "meta";
this._getItemAtIndex(index)?.appendChild(spinner);
}
private _handleSearchChange(ev: CustomEvent): void {
@ -740,20 +735,10 @@ export class QuickBar extends LitElement {
}
}
const additionalItems = [
{
path: "",
primaryText: this.hass.localize("ui.panel.config.info.shortcuts"),
action: () => showShortcutsDialog(this),
iconPath: mdiKeyboard,
},
];
return this._finalizeNavigationCommands([
...panelItems,
...sectionItems,
...supervisorItems,
...additionalItems,
]);
}
@ -830,12 +815,12 @@ export class QuickBar extends LitElement {
const categoryKey: CommandItem["categoryKey"] = "navigation";
const navItem = {
...item,
iconPath: mdiEarth,
categoryText: this.hass.localize(
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
),
action: () => navigate(item.path),
...item,
};
return {

View File

@ -1,228 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import { createCloseHeading } from "../../components/ha-dialog";
import type { HomeAssistant } from "../../types";
import { haStyleDialog } from "../../resources/styles";
import "../../components/ha-alert";
import "../../components/chips/ha-assist-chip";
import type { LocalizeKeys } from "../../common/translations/localize";
interface Text {
type: "text";
key: LocalizeKeys;
}
type ShortcutString = string | { key: LocalizeKeys };
interface Shortcut {
type: "shortcut";
shortcut: ShortcutString[];
key: LocalizeKeys;
}
interface Section {
key: LocalizeKeys;
items: (Text | Shortcut)[];
}
const _SHORTCUTS: Section[] = [
{
key: "ui.dialogs.shortcuts.searching.title",
items: [
{ type: "text", key: "ui.dialogs.shortcuts.searching.on_any_page" },
{
type: "shortcut",
shortcut: ["C"],
key: "ui.dialogs.shortcuts.searching.search_command",
},
{
type: "shortcut",
shortcut: ["E"],
key: "ui.dialogs.shortcuts.searching.search_entities",
},
{
type: "shortcut",
shortcut: ["D"],
key: "ui.dialogs.shortcuts.searching.search_devices",
},
{
type: "text",
key: "ui.dialogs.shortcuts.searching.on_pages_with_tables",
},
{
type: "shortcut",
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "F"],
key: "ui.dialogs.shortcuts.searching.search_in_table",
},
],
},
{
key: "ui.dialogs.shortcuts.assist.title",
items: [
{
type: "shortcut",
shortcut: ["A"],
key: "ui.dialogs.shortcuts.assist.open_assist",
},
],
},
{
key: "ui.dialogs.shortcuts.charts.title",
items: [
{
type: "shortcut",
shortcut: [
{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" },
{ key: "ui.dialogs.shortcuts.shortcuts.drag" },
],
key: "ui.dialogs.shortcuts.charts.drag_to_zoom",
},
{
type: "shortcut",
shortcut: [
{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" },
{ key: "ui.dialogs.shortcuts.shortcuts.scroll_wheel" },
],
key: "ui.dialogs.shortcuts.charts.scroll_to_zoom",
},
{
type: "shortcut",
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.double_click" }],
key: "ui.dialogs.shortcuts.charts.double_click",
},
],
},
{
key: "ui.dialogs.shortcuts.other.title",
items: [
{
type: "shortcut",
shortcut: ["M"],
key: "ui.dialogs.shortcuts.other.my_link",
},
],
},
];
@customElement("dialog-shortcuts")
class DialogShortcuts extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
public async showDialog(): Promise<void> {
this._opened = true;
}
public async closeDialog(): Promise<void> {
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _renderShortcut(
shortcuts: ShortcutString[],
translationKey: LocalizeKeys
) {
const keys = shortcuts.map((shortcut) =>
typeof shortcut === "string" ? shortcut : this.hass.localize(shortcut.key)
);
return html`
<div class="shortcut">
${keys.map((key) => html` <span>${key.toUpperCase()}</span>`)}
${this.hass.localize(translationKey)}
</div>
`;
}
protected render() {
if (!this._opened) {
return nothing;
}
return html`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
defaultAction="ignore"
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.shortcuts.title")
)}
>
<div class="content">
${_SHORTCUTS.map(
(section) => html`
<h3>${this.hass.localize(section.key)}</h3>
<div class="items">
${section.items.map((item) => {
if (item.type === "text") {
return html`<p>${this.hass.localize(item.key)}</p>`;
}
if (item.type === "shortcut") {
return this._renderShortcut(item.shortcut, item.key);
}
return nothing;
})}
</div>
`
)}
</div>
<ha-alert>
${this.hass.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
user_profile: html`<a href="/profile/general#shortcuts"
>${this.hass.localize(
"ui.dialogs.shortcuts.enable_shortcuts_hint_user_profile"
)}</a
>`,
})}
</ha-alert>
</ha-dialog>
`;
}
static styles = [
haStyleDialog,
css`
ha-dialog {
--dialog-z-index: 15;
}
h3:first-of-type {
margin-top: 0;
}
.content {
margin-bottom: 24px;
}
.shortcut {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
margin: 4px 0;
}
span {
padding: 8px;
border: 1px solid var(--outline-color);
border-radius: 8px;
}
.items p {
margin-bottom: 8px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-shortcuts": DialogShortcuts;
}
}

View File

@ -1,8 +0,0 @@
import { fireEvent } from "../../common/dom/fire_event";
export const showShortcutsDialog = (element: HTMLElement) =>
fireEvent(element, "show-dialog", {
dialogTag: "dialog-shortcuts",
dialogImport: () => import("./dialog-shortcuts"),
dialogParams: {},
});

View File

@ -90,9 +90,6 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
this._previousSteps = [];
this._nextStep = undefined;
this._step = STEP.INIT;
this._language = undefined;
this._languages = [];
this._localOption = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}

View File

@ -103,9 +103,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
</ha-select>
<ha-button @click=${this._testWakeWord}>
<ha-svg-icon slot="icon" .path=${mdiMicrophone}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.success.test_wakeword"
)}
Test
</ha-button>
</div>`
: nothing}
@ -128,9 +126,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
</ha-select>
<ha-button @click=${this._openPipeline}>
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.success.edit_pipeline"
)}
Edit
</ha-button>
</div>`
: nothing}
@ -146,20 +142,14 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
></ha-tts-voice-picker>
<ha-button @click=${this._testTts}>
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.success.try_tts"
)}
Try
</ha-button>
</div>`
: nothing}
</div>
</div>
<div class="footer">
<ha-button @click=${this._close} unelevated
>${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.success.done"
)}</ha-button
>
<ha-button @click=${this._close} unelevated>Done</ha-button>
</div>`;
}
@ -258,7 +248,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
}
await assistSatelliteAnnounce(this.hass, this.assistEntityId, {
message,
preannounce: false,
preannounce_media_id: null,
});
}

View File

@ -2,3 +2,9 @@ import "@webcomponents/scoped-custom-element-registry/scoped-custom-element-regi
import "../layouts/home-assistant";
import("../resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then(
({ setCancelSyntheticClickEvents, setPassiveTouchGestures }) => {
setCancelSyntheticClickEvents(false);
setPassiveTouchGestures(true);
}
);

View File

@ -1,3 +1,6 @@
import "../auth/ha-authorize";
import("../resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then(
({ setCancelSyntheticClickEvents }) => setCancelSyntheticClickEvents(false)
);

View File

@ -10,6 +10,10 @@ import { createCustomPanelElement } from "../util/custom-panel/create-custom-pan
import { loadCustomPanel } from "../util/custom-panel/load-custom-panel";
import { setCustomPanelProperties } from "../util/custom-panel/set-custom-panel-properties";
import("@polymer/polymer/lib/utils/settings").then(
({ setCancelSyntheticClickEvents }) => setCancelSyntheticClickEvents(false)
);
declare global {
interface Window {
loadES5Adapter: () => Promise<unknown>;

View File

@ -1,6 +1,9 @@
import "../onboarding/ha-onboarding";
import("../resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then(
({ setCancelSyntheticClickEvents }) => setCancelSyntheticClickEvents(false)
);
declare global {
interface Window {

View File

@ -7,6 +7,7 @@
script.src = src;
return document.head.appendChild(script);
}
window.polymerSkipLoadingFontRoboto = true;
if (!("attachShadow" in Element.prototype)) {
_ls("/static/polyfills/webcomponents-bundle.js", true);
_ls("/static/polyfills/lit-polyfill-support.js", true);

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