mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-28 20:17:18 +00:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bf5833262 | |||
| 8c9da19935 | |||
| 4b1eeb0eb1 | |||
| f7ffdabe5d | |||
| 678ee7e82a | |||
| ee72f4818d | |||
| ccbd9c1f24 | |||
| 84135a9424 | |||
| 4ebc334298 | |||
| 3c4c3e39e5 | |||
| 1267003b42 | |||
| 733359c869 | |||
| 50b6e07ae5 | |||
| 1b62a7cff8 | |||
| c293cf56f6 | |||
| 2ec6f3615d | |||
| d3b92059e5 | |||
| 72e69f6291 | |||
| 3bff97595f | |||
| 19b1d03cd1 | |||
| f04557688c | |||
| d6953ea1bc | |||
| 4501db18c7 | |||
| c68bcc5b32 | |||
| 3753c7d313 | |||
| 4a2ef18375 | |||
| 051da41eec | |||
| 9b1a679f21 | |||
| 905a0f957c | |||
| d9a687b79c | |||
| 3b56497134 | |||
| ed0ec871ce | |||
| 29f5362182 | |||
| 5096bab26c | |||
| ce2892cab9 | |||
| 57748c15de | |||
| ecb6a33c86 | |||
| d34921ff6d | |||
| e6c9e81082 | |||
| aa13c6fa53 | |||
| d47738aa24 | |||
| d473ee1084 | |||
| 0372ed932f | |||
| 2baa044db5 | |||
| 3d04046bcc | |||
| 8e860cb17d | |||
| c41d7ff923 | |||
| c22fc1021a | |||
| 6344233934 | |||
| 23441d593b | |||
| 8393ed5fd4 | |||
| 09afe9bb51 | |||
| fb8d6062c5 | |||
| f93ae58b83 | |||
| 7626b26b2d | |||
| a1bf30e501 | |||
| 37a45d1729 | |||
| 6962a915a3 | |||
| de3e2bcafa | |||
| e732280b70 | |||
| 4fb3453f73 | |||
| 0f9cb9c13e | |||
| 7aa235c6af | |||
| e8ddae8189 | |||
| c26e59f19c | |||
| 83aa06cb18 | |||
| 9d3d0dac48 | |||
| 8da1154924 | |||
| eb588075b8 | |||
| bdeaf10d74 | |||
| bec0d19fc9 | |||
| 325a7974c2 | |||
| fab1fde6e3 | |||
| 0e9564e676 | |||
| 244eb75049 | |||
| 644bb016d6 | |||
| 02dbcf0946 | |||
| f7df4d8a90 | |||
| e00ced23ee | |||
| f5cc2104ef | |||
| 161dd26b4d | |||
| 29cee99f10 | |||
| 47341e93fc | |||
| cbae7d6e2f | |||
| ebff35d17f |
@@ -251,7 +251,6 @@ For browser support, API details, and current specifications, refer to these aut
|
||||
**Available Dialog Types:**
|
||||
|
||||
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
|
||||
- `ha-md-dialog` - Material Design 3 dialog component
|
||||
- `ha-dialog` - Legacy component (still widely used)
|
||||
|
||||
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
||||
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
|
||||
@@ -31,4 +31,7 @@ module.exports = {
|
||||
isDevContainer() {
|
||||
return isTrue(process.env.DEV_CONTAINER);
|
||||
},
|
||||
jsMinifier() {
|
||||
return (process.env.JS_MINIFIER || "swc").toLowerCase();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -80,7 +80,13 @@ const doneHandler = (done) => (err, stats) => {
|
||||
console.log(stats.toString("minimal"));
|
||||
}
|
||||
|
||||
log(`Build done @ ${new Date().toLocaleTimeString()}`);
|
||||
const durationMs =
|
||||
stats?.startTime && stats?.endTime ? stats.endTime - stats.startTime : 0;
|
||||
const durationLabel = durationMs
|
||||
? ` (${(durationMs / 1000).toFixed(1)}s, minifier: ${env.jsMinifier()})`
|
||||
: ` (minifier: ${env.jsMinifier()})`;
|
||||
|
||||
log(`Build done @ ${new Date().toLocaleTimeString()}${durationLabel}`);
|
||||
|
||||
if (done) {
|
||||
done();
|
||||
|
||||
@@ -13,6 +13,7 @@ const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
|
||||
const log = require("fancy-log");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const WebpackBar = require("webpackbar/rspack");
|
||||
const env = require("./env.cjs");
|
||||
const paths = require("./paths.cjs");
|
||||
const bundle = require("./bundle.cjs");
|
||||
|
||||
@@ -100,11 +101,20 @@ const createRspackConfig = ({
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
extractComments: true,
|
||||
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
|
||||
}),
|
||||
env.jsMinifier() === "terser"
|
||||
? new TerserPlugin({
|
||||
parallel: true,
|
||||
extractComments: true,
|
||||
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
|
||||
})
|
||||
: new rspack.SwcJsMinimizerRspackPlugin({
|
||||
extractComments: true,
|
||||
minimizerOptions: {
|
||||
ecma: latestBuild ? 2015 : 5,
|
||||
module: latestBuild,
|
||||
format: { comments: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
let changeFunction;
|
||||
let sidebarChangeCallback;
|
||||
|
||||
export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("frontend/get_user_data", () => ({
|
||||
value: null,
|
||||
}));
|
||||
hass.mockWS("frontend/get_user_data", () => ({ value: null }));
|
||||
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
|
||||
if (key === "sidebar") {
|
||||
changeFunction?.({
|
||||
sidebarChangeCallback?.({
|
||||
value: {
|
||||
panelOrder: value.panelOrder || [],
|
||||
hiddenPanels: value.hiddenPanels || [],
|
||||
@@ -16,14 +14,11 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
|
||||
changeFunction = onChange;
|
||||
onChange?.({
|
||||
value: {
|
||||
panelOrder: [],
|
||||
hiddenPanels: [],
|
||||
},
|
||||
});
|
||||
hass.mockWS("frontend/subscribe_user_data", (msg, _hass, onChange) => {
|
||||
if (msg.key === "sidebar") {
|
||||
sidebarChangeCallback = onChange;
|
||||
}
|
||||
onChange?.({ value: null });
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
@@ -48,4 +43,5 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
|
||||
hass.mockWS("frontend/get_themes", (_msg, currentHass) => currentHass.themes);
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ export const mockLovelace = (
|
||||
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
||||
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));
|
||||
};
|
||||
|
||||
customElements.whenDefined("hui-root").then(() => {
|
||||
|
||||
@@ -10,7 +10,9 @@ As a community, we are proud of our logo. Follow these guidelines to ensure it a
|
||||
|
||||

|
||||
|
||||
Please note that this logo is not released under the CC license. All rights reserved.
|
||||
<ha-alert alert-type="info">
|
||||
This logo is trademarked and the property of the Open Home Foundation. This means it is not available for commercial use without express written permission from the foundation. We regard commercial use as anything designed to market or promote a product, software or service that is for sale. Please contact <a href="mailto:partner@openhomefoundation.org">partner@openhomefoundation.org</a> for further information
|
||||
</ha-alert>
|
||||
|
||||
# Design
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import "../../../../src/components/ha-alert";
|
||||
@@ -18,7 +18,7 @@ The Home Assistant interface is based on Material Design. It's a design system c
|
||||
|
||||
We want to make it as easy for designers to contribute as it is for developers. There’s a lot a designer can contribute to:
|
||||
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat-design" rel="noopener noreferrer" target="_blank">Discord #designers channel</a>. If you can't see the channel, make sure you set the correct role in Channels & Roles.
|
||||
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
|
||||
|
||||
+8
-8
@@ -29,7 +29,7 @@
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/commands": "6.10.2",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.0.0-ha.2",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.0",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -132,7 +132,7 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.8",
|
||||
"ua-parser-js": "2.0.9",
|
||||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
@@ -154,8 +154,8 @@
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.1",
|
||||
"@rspack/core": "1.7.4",
|
||||
"@rsdoctor/rspack-plugin": "1.5.2",
|
||||
"@rspack/core": "1.7.5",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -191,14 +191,14 @@
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"glob": "13.0.0",
|
||||
"glob": "13.0.1",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "27.4.0",
|
||||
"jsdom": "28.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -235,6 +235,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.13.0"
|
||||
"node": "24.13.1"
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
OUT_ROOT="$ROOT_DIR/hass_frontend"
|
||||
OUT_LATEST="$OUT_ROOT/frontend_latest"
|
||||
OUT_ES5="$OUT_ROOT/frontend_es5"
|
||||
|
||||
bytes_dir() {
|
||||
if [ -d "$1" ]; then
|
||||
du -sb "$1" | cut -f1
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
run_build() {
|
||||
minifier="$1"
|
||||
printf "\n==> Building with %s\n" "$minifier"
|
||||
start_time=$(date +%s)
|
||||
JS_MINIFIER="$minifier" "$ROOT_DIR/script/build_frontend"
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
|
||||
latest_size=$(bytes_dir "$OUT_LATEST")
|
||||
es5_size=$(bytes_dir "$OUT_ES5")
|
||||
total_size=$(bytes_dir "$OUT_ROOT")
|
||||
|
||||
printf "%s|%s|%s|%s\n" "$minifier" "$duration" "$latest_size" "$es5_size" >> "$ROOT_DIR/temp/minifier_benchmark.tsv"
|
||||
printf " duration: %ss\n" "$duration"
|
||||
printf " frontend_latest: %s bytes\n" "$latest_size"
|
||||
printf " frontend_es5: %s bytes\n" "$es5_size"
|
||||
printf " hass_frontend: %s bytes\n" "$total_size"
|
||||
}
|
||||
|
||||
mkdir -p "$ROOT_DIR/temp"
|
||||
rm -f "$ROOT_DIR/temp/minifier_benchmark.tsv"
|
||||
|
||||
run_build swc
|
||||
run_build terser
|
||||
|
||||
printf "\n==> Summary (minifier | seconds | latest bytes | es5 bytes)\n"
|
||||
cat "$ROOT_DIR/temp/minifier_benchmark.tsv"
|
||||
@@ -116,3 +116,6 @@ export const UNIT_F = "°F";
|
||||
|
||||
/** Entity ID of the default view. */
|
||||
export const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
|
||||
|
||||
/** String to visually separate labels on UI */
|
||||
export const STRINGS_SEPARATOR_DOT = " · ";
|
||||
|
||||
@@ -3,13 +3,14 @@ import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValuePart } from "../../types";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
formatNumberToParts,
|
||||
getNumberFormatOptions,
|
||||
isNumericFromAttributes,
|
||||
} from "../number/format_number";
|
||||
@@ -51,8 +52,36 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
attributes: any,
|
||||
state: string
|
||||
): string => {
|
||||
const parts = computeStateToPartsFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
sensorNumericDeviceClasses,
|
||||
config,
|
||||
entity,
|
||||
entityId,
|
||||
attributes,
|
||||
state
|
||||
);
|
||||
return parts.map((part) => part.value).join("");
|
||||
};
|
||||
|
||||
const computeStateToPartsFromEntityAttributes = (
|
||||
localize: LocalizeFunc,
|
||||
locale: FrontendLocaleData,
|
||||
sensorNumericDeviceClasses: string[],
|
||||
config: HassConfig,
|
||||
entity: EntityRegistryDisplayEntry | undefined,
|
||||
entityId: string,
|
||||
attributes: any,
|
||||
state: string
|
||||
): ValuePart[] => {
|
||||
if (state === UNKNOWN || state === UNAVAILABLE) {
|
||||
return localize(`state.default.${state}`);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: localize(`state.default.${state}`),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const domain = computeDomain(entityId);
|
||||
@@ -73,19 +102,27 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
DURATION_UNITS.includes(attributes.unit_of_measurement)
|
||||
) {
|
||||
try {
|
||||
return formatDuration(
|
||||
locale,
|
||||
state,
|
||||
attributes.unit_of_measurement,
|
||||
entity?.display_precision
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDuration(
|
||||
locale,
|
||||
state,
|
||||
attributes.unit_of_measurement,
|
||||
entity?.display_precision
|
||||
),
|
||||
},
|
||||
];
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
|
||||
// state is monetary
|
||||
if (attributes.device_class === "monetary") {
|
||||
let parts: Record<string, string>[] = [];
|
||||
try {
|
||||
return formatNumber(state, locale, {
|
||||
parts = formatNumberToParts(state, locale, {
|
||||
style: "currency",
|
||||
currency: attributes.unit_of_measurement,
|
||||
minimumFractionDigits: 2,
|
||||
@@ -98,8 +135,34 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
|
||||
const TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
};
|
||||
|
||||
const valueParts: ValuePart[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
const type = TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
|
||||
if (type === "value" && last?.type === "value") {
|
||||
last.value += part.value;
|
||||
} else {
|
||||
valueParts.push({ type, value: part.value });
|
||||
}
|
||||
}
|
||||
|
||||
return valueParts;
|
||||
}
|
||||
|
||||
// default processing of numeric values
|
||||
const value = formatNumber(
|
||||
state,
|
||||
locale,
|
||||
@@ -114,10 +177,14 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
attributes.unit_of_measurement;
|
||||
|
||||
if (unit) {
|
||||
return `${value}${blankBeforeUnit(unit, locale)}${unit}`;
|
||||
return [
|
||||
{ type: "value", value: value },
|
||||
{ type: "literal", value: blankBeforeUnit(unit, locale) },
|
||||
{ type: "unit", value: unit },
|
||||
];
|
||||
}
|
||||
|
||||
return value;
|
||||
return [{ type: "value", value: value }];
|
||||
}
|
||||
|
||||
if (["date", "input_datetime", "time"].includes(domain)) {
|
||||
@@ -129,36 +196,51 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
const components = state.split(" ");
|
||||
if (components.length === 2) {
|
||||
// Date and time.
|
||||
return formatDateTime(
|
||||
new Date(components.join("T")),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDateTime(
|
||||
new Date(components.join("T")),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (components.length === 1) {
|
||||
if (state.includes("-")) {
|
||||
// Date only.
|
||||
return formatDate(
|
||||
new Date(`${state}T00:00`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDate(
|
||||
new Date(`${state}T00:00`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (state.includes(":")) {
|
||||
// Time only.
|
||||
const now = new Date();
|
||||
return formatTime(
|
||||
new Date(`${now.toISOString().split("T")[0]}T${state}`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatTime(
|
||||
new Date(`${now.toISOString().split("T")[0]}T${state}`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
return state;
|
||||
return [{ type: "value", value: state }];
|
||||
} catch (_e) {
|
||||
// Formatting methods may throw error if date parsing doesn't go well,
|
||||
// just return the state string in that case.
|
||||
return state;
|
||||
return [{ type: "value", value: state }];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,25 +264,58 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
) {
|
||||
try {
|
||||
return formatDateTime(new Date(state), locale, config);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDateTime(new Date(state), locale, config),
|
||||
},
|
||||
];
|
||||
} catch (_err) {
|
||||
return state;
|
||||
return [{ type: "value", value: state }];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(entity?.translation_key &&
|
||||
localize(
|
||||
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
|
||||
)) ||
|
||||
// Return device class translation
|
||||
(attributes.device_class &&
|
||||
localize(
|
||||
`component.${domain}.entity_component.${attributes.device_class}.state.${state}`
|
||||
)) ||
|
||||
// Return default translation
|
||||
localize(`component.${domain}.entity_component._.state.${state}`) ||
|
||||
// We don't know! Return the raw state.
|
||||
state
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value:
|
||||
(entity?.translation_key &&
|
||||
localize(
|
||||
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
|
||||
)) ||
|
||||
// Return device class translation
|
||||
(attributes.device_class &&
|
||||
localize(
|
||||
`component.${domain}.entity_component.${attributes.device_class}.state.${state}`
|
||||
)) ||
|
||||
// Return default translation
|
||||
localize(`component.${domain}.entity_component._.state.${state}`) ||
|
||||
// We don't know! Return the raw state.
|
||||
state,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const computeStateToParts = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
locale: FrontendLocaleData,
|
||||
sensorNumericDeviceClasses: string[],
|
||||
config: HassConfig,
|
||||
entities: HomeAssistant["entities"],
|
||||
state?: string
|
||||
): ValuePart[] => {
|
||||
const entity = entities?.[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
return computeStateToPartsFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
sensorNumericDeviceClasses,
|
||||
config,
|
||||
entity,
|
||||
stateObj.entity_id,
|
||||
stateObj.attributes,
|
||||
state !== undefined ? state : stateObj.state
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { NumberFormat } from "../../data/translation";
|
||||
import { round } from "./round";
|
||||
|
||||
/**
|
||||
* Returns true if the entity is considered numeric based on the attributes it has
|
||||
@@ -52,7 +51,22 @@ export const formatNumber = (
|
||||
num: string | number,
|
||||
localeOptions?: FrontendLocaleData,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): string => {
|
||||
): string =>
|
||||
formatNumberToParts(num, localeOptions, options)
|
||||
.map((part) => part.value)
|
||||
.join("");
|
||||
|
||||
/**
|
||||
* Returns an array of objects containing the formatted number in parts
|
||||
* Similar to Intl.NumberFormat.prototype.formatToParts()
|
||||
*
|
||||
* Input params - same as for formatNumber()
|
||||
*/
|
||||
export const formatNumberToParts = (
|
||||
num: string | number,
|
||||
localeOptions?: FrontendLocaleData,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): any[] => {
|
||||
const locale = localeOptions
|
||||
? numberFormatToLocale(localeOptions)
|
||||
: undefined;
|
||||
@@ -71,7 +85,7 @@ export const formatNumber = (
|
||||
return new Intl.NumberFormat(
|
||||
locale,
|
||||
getDefaultFormatOptions(num, options)
|
||||
).format(Number(num));
|
||||
).formatToParts(Number(num));
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -86,15 +100,10 @@ export const formatNumber = (
|
||||
...options,
|
||||
useGrouping: false,
|
||||
})
|
||||
).format(Number(num));
|
||||
).formatToParts(Number(num));
|
||||
}
|
||||
|
||||
if (typeof num === "string") {
|
||||
return num;
|
||||
}
|
||||
return `${round(num, options?.maximumFractionDigits).toString()}${
|
||||
options?.style === "currency" ? ` ${options.currency}` : ""
|
||||
}`;
|
||||
return [{ type: "literal", value: num }];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
const SI_PREFIX_MULTIPLIERS: Record<string, number> = {
|
||||
T: 1e12,
|
||||
G: 1e9,
|
||||
M: 1e6,
|
||||
k: 1e3,
|
||||
m: 1e-3,
|
||||
"\u00B5": 1e-6, // µ (micro sign)
|
||||
"\u03BC": 1e-6, // μ (greek small letter mu)
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize a numeric value by detecting SI unit prefixes (T, G, M, k, m, µ).
|
||||
* Only applies when the unit is longer than 1 character and starts with a
|
||||
* recognized prefix, avoiding false positives on standalone units like "m" (meters).
|
||||
*/
|
||||
export const normalizeValueBySIPrefix = (
|
||||
value: number,
|
||||
unit: string | undefined
|
||||
): number => {
|
||||
if (!unit || unit.length <= 1) {
|
||||
return value;
|
||||
}
|
||||
const prefix = unit[0];
|
||||
if (prefix in SI_PREFIX_MULTIPLIERS) {
|
||||
return value * SI_PREFIX_MULTIPLIERS[prefix];
|
||||
}
|
||||
return value;
|
||||
};
|
||||
@@ -12,6 +12,10 @@ export type FormatEntityStateFunc = (
|
||||
stateObj: HassEntity,
|
||||
state?: string
|
||||
) => string;
|
||||
export type FormatEntityStateToPartsFunc = (
|
||||
stateObj: HassEntity,
|
||||
state?: string
|
||||
) => ValuePart[];
|
||||
export type FormatEntityAttributeValueFunc = (
|
||||
stateObj: HassEntity,
|
||||
attribute: string,
|
||||
@@ -46,12 +50,13 @@ export const computeFormatFunctions = async (
|
||||
sensorNumericDeviceClasses: string[]
|
||||
): Promise<{
|
||||
formatEntityState: FormatEntityStateFunc;
|
||||
formatEntityStateToParts: FormatEntityStateToPartsFunc;
|
||||
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
|
||||
formatEntityAttributeValueToParts: FormatEntityAttributeValueToPartsFunc;
|
||||
formatEntityAttributeName: FormatEntityAttributeNameFunc;
|
||||
formatEntityName: FormatEntityNameFunc;
|
||||
}> => {
|
||||
const { computeStateDisplay } =
|
||||
const { computeStateDisplay, computeStateToParts } =
|
||||
await import("../entity/compute_state_display");
|
||||
const {
|
||||
computeAttributeValueDisplay,
|
||||
@@ -70,6 +75,16 @@ export const computeFormatFunctions = async (
|
||||
entities,
|
||||
state
|
||||
),
|
||||
formatEntityStateToParts: (stateObj, state) =>
|
||||
computeStateToParts(
|
||||
localize,
|
||||
stateObj,
|
||||
locale,
|
||||
sensorNumericDeviceClasses,
|
||||
config,
|
||||
entities,
|
||||
state
|
||||
),
|
||||
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||
computeAttributeValueDisplay(
|
||||
localize,
|
||||
|
||||
@@ -572,6 +572,7 @@ export class StatisticsChart extends LitElement {
|
||||
let firstSum: number | null | undefined = null;
|
||||
stats.forEach((stat) => {
|
||||
const startDate = new Date(stat.start);
|
||||
const endDate = new Date(stat.end);
|
||||
if (prevDate === startDate) {
|
||||
return;
|
||||
}
|
||||
@@ -601,10 +602,25 @@ export class StatisticsChart extends LitElement {
|
||||
dataValues.push(val);
|
||||
});
|
||||
if (!this._hiddenStats.has(statistic_id)) {
|
||||
pushData(startDate, new Date(stat.end), dataValues);
|
||||
pushData(
|
||||
startDate,
|
||||
endDate.getTime() < endTime.getTime() ? endDate : endTime,
|
||||
dataValues
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Close out the last stat segment at prevEndTime
|
||||
const lastEndTime = prevEndTime;
|
||||
const lastValues = prevValues;
|
||||
if (lastEndTime && lastValues) {
|
||||
statDataSets.forEach((d, i) => {
|
||||
d.data!.push(
|
||||
this._transformDataValue([lastEndTime, ...lastValues[i]!])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Append current state if viewing recent data
|
||||
const now = new Date();
|
||||
// allow 10m of leeway for "now", because stats are 5 minute aggregated
|
||||
@@ -619,16 +635,6 @@ export class StatisticsChart extends LitElement {
|
||||
isFinite(currentValue) &&
|
||||
!this._hiddenStats.has(statistic_id)
|
||||
) {
|
||||
// First, close out the last stat segment at prevEndTime
|
||||
const lastEndTime = prevEndTime;
|
||||
const lastValues = prevValues;
|
||||
if (lastEndTime && lastValues) {
|
||||
statDataSets.forEach((d, i) => {
|
||||
d.data!.push(
|
||||
this._transformDataValue([lastEndTime, ...lastValues[i]!])
|
||||
);
|
||||
});
|
||||
}
|
||||
// Then push the current state at now
|
||||
statTypes.forEach((type, i) => {
|
||||
const val: (number | null)[] = [];
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -636,7 +637,7 @@ export class HaDataTable extends LitElement {
|
||||
.map(
|
||||
([key2, column2], i) =>
|
||||
html`${i !== 0
|
||||
? " · "
|
||||
? STRINGS_SEPARATOR_DOT
|
||||
: nothing}${column2.template
|
||||
? column2.template(row)
|
||||
: row[key2]}`
|
||||
@@ -1192,6 +1193,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
.mdc-data-table__cell--numeric {
|
||||
text-align: var(--float-end);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--icon {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
sortDeviceAutomations,
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-md-select";
|
||||
import "../ha-md-select-option";
|
||||
@@ -192,7 +192,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
this._renderEmpty = false;
|
||||
}
|
||||
|
||||
private _automationChanged(ev: CustomEvent<{ value: string }>) {
|
||||
private _automationChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (!value || NO_AUTOMATION_KEY === value) {
|
||||
|
||||
@@ -9,16 +9,7 @@ import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
isNumericState,
|
||||
} from "../../common/number/format_number";
|
||||
import {
|
||||
isUnavailableState,
|
||||
UNAVAILABLE,
|
||||
UNKNOWN,
|
||||
} from "../../data/entity/entity";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -180,16 +171,11 @@ export class HaStateLabelBadge extends LitElement {
|
||||
}
|
||||
// eslint-disable-next-line: disable=no-fallthrough
|
||||
default:
|
||||
return entityState.state === UNKNOWN ||
|
||||
entityState.state === UNAVAILABLE
|
||||
return isUnavailableState(entityState.state)
|
||||
? "—"
|
||||
: isNumericState(entityState)
|
||||
? formatNumber(
|
||||
entityState.state,
|
||||
this.hass!.locale,
|
||||
getNumberFormatOptions(entityState, entry)
|
||||
)
|
||||
: this.hass!.formatEntityState(entityState);
|
||||
: this.hass!.formatEntityStateToParts(entityState).find(
|
||||
(part) => part.type === "value"
|
||||
)?.value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +224,11 @@ export class HaStateLabelBadge extends LitElement {
|
||||
if (domain === "timer") {
|
||||
return secondsToDuration(_timerTimeRemaining);
|
||||
}
|
||||
return entityState.attributes.unit_of_measurement || null;
|
||||
return (
|
||||
this.hass!.formatEntityStateToParts(entityState).find(
|
||||
(part) => part.type === "unit"
|
||||
)?.value || null
|
||||
);
|
||||
}
|
||||
|
||||
private _clearInterval() {
|
||||
|
||||
@@ -163,7 +163,7 @@ export class HaAreaPicker extends LitElement {
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.area-picker.add_new_sugestion",
|
||||
"ui.components.area-picker.add_new_suggestion",
|
||||
{
|
||||
name: searchString,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-items-display-editor";
|
||||
@@ -200,7 +200,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private async _areaDisplayChanged(ev: CustomEvent<{ value: DisplayValue }>) {
|
||||
private async _areaDisplayChanged(ev: ValueChangedEvent<DisplayValue>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
const currentFloorId = (ev.currentTarget as any).floorId;
|
||||
|
||||
@@ -6,8 +6,8 @@ import { formatLanguageCode } from "../common/language/format_language";
|
||||
import type { AssistPipeline } from "../data/assist_pipeline";
|
||||
import { listAssistPipelines } from "../data/assist_pipeline";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
|
||||
const PREFERRED = "preferred";
|
||||
const LAST_USED = "last_used";
|
||||
@@ -94,7 +94,7 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: CustomEvent<{ value: string }>): void {
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-select";
|
||||
@@ -276,11 +277,11 @@ export class HaBaseTimeInput extends LitElement {
|
||||
fireEvent(this, "value-changed");
|
||||
}
|
||||
|
||||
private _valueChanged(ev: InputEvent | CustomEvent<{ value: string }>): void {
|
||||
private _valueChanged(ev: InputEvent | HaSelectSelectEvent): void {
|
||||
const textField = ev.currentTarget as HaTextField;
|
||||
this[textField.name] =
|
||||
textField.name === "amPm"
|
||||
? (ev as CustomEvent<{ value: string }>).detail.value
|
||||
? (ev as HaSelectSelectEvent).detail.value
|
||||
: Number(textField.value);
|
||||
const value: TimeChangedEvent = {
|
||||
hours: this.hours,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { stringCompare } from "../common/string/compare";
|
||||
import type { Blueprint, BlueprintDomain, Blueprints } from "../data/blueprint";
|
||||
import { fetchBlueprints } from "../data/blueprint";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
|
||||
@customElement("ha-blueprint-picker")
|
||||
@@ -76,7 +77,7 @@ class HaBluePrintPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _blueprintChanged(ev: CustomEvent<{ value: string }>) {
|
||||
private _blueprintChanged(ev: HaSelectSelectEvent) {
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (newValue !== this.value) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import memoizeOne from "memoize-one";
|
||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
@@ -224,7 +224,7 @@ export class HaColorPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent<{ value?: string }>) {
|
||||
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
|
||||
ev.stopPropagation();
|
||||
const selected = ev.detail.value;
|
||||
const normalized =
|
||||
|
||||
@@ -89,7 +89,7 @@ export class HaControlSelectMenu extends LitElement {
|
||||
private _renderOption = (option: SelectOption) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${option.value}
|
||||
class=${this.value === option.value ? "selected" : ""}
|
||||
.selected=${this.value === option.value}
|
||||
>${option.iconPath
|
||||
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
|
||||
: option.icon
|
||||
@@ -263,15 +263,6 @@ export class HaControlSelectMenu extends LitElement {
|
||||
cursor: not-allowed;
|
||||
color: var(--disabled-color);
|
||||
}
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
ha-dropdown-item.selected:hover {
|
||||
background-color: var(--ha-color-fill-primary-quiet-hover);
|
||||
}
|
||||
|
||||
ha-dropdown::part(menu) {
|
||||
min-width: var(--control-select-menu-width);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-option
|
||||
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-select";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@@ -234,7 +234,7 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: CustomEvent<{ value: string }>): void {
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
|
||||
@@ -65,6 +65,10 @@ export class HaDateRangePicker extends LitElement {
|
||||
@property({ attribute: "time-picker", type: Boolean })
|
||||
public timePicker = false;
|
||||
|
||||
public open(): void {
|
||||
this._openPicker();
|
||||
}
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public minimal = false;
|
||||
@@ -306,6 +310,15 @@ export class HaDateRangePicker extends LitElement {
|
||||
return dateRangePicker.vueComponent.$children[0];
|
||||
}
|
||||
|
||||
private _openPicker() {
|
||||
if (!this._dateRangePicker.open) {
|
||||
const datePicker = this.shadowRoot!.querySelector(
|
||||
"date-range-picker div.date-range-inputs"
|
||||
) as any;
|
||||
datePicker?.click();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleInputClick() {
|
||||
// close the date picker, so it will open again on the click event
|
||||
if (this._dateRangePicker.open) {
|
||||
|
||||
@@ -7,8 +7,9 @@ import { nextRender } from "../common/util/render-status";
|
||||
import { haStyleDialog } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { DatePickerDialogParams } from "./ha-date-input";
|
||||
import "./ha-dialog";
|
||||
import "./ha-button";
|
||||
import "./ha-dialog-footer";
|
||||
import "./ha-wa-dialog";
|
||||
|
||||
@customElement("ha-dialog-date-picker")
|
||||
export class HaDialogDatePicker extends LitElement {
|
||||
@@ -22,6 +23,8 @@ export class HaDialogDatePicker extends LitElement {
|
||||
|
||||
@state() private _params?: DatePickerDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _value?: string;
|
||||
|
||||
public async showDialog(params: DatePickerDialogParams): Promise<void> {
|
||||
@@ -30,9 +33,14 @@ export class HaDialogDatePicker extends LitElement {
|
||||
await nextRender();
|
||||
this._params = params;
|
||||
this._value = params.value;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -41,7 +49,13 @@ export class HaDialogDatePicker extends LitElement {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-dialog open @closed=${this.closeDialog}>
|
||||
return html`<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
width="small"
|
||||
without-header
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<app-datepicker
|
||||
.value=${this._value}
|
||||
.min=${this._params.min}
|
||||
@@ -50,35 +64,40 @@ export class HaDialogDatePicker extends LitElement {
|
||||
@datepicker-value-updated=${this._valueChanged}
|
||||
.firstDayOfWeek=${this._params.firstWeekday}
|
||||
></app-datepicker>
|
||||
${this._params.canClear
|
||||
? html`<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._clear}
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.clear")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this._setToday}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.today")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="primaryAction"
|
||||
dialogaction="cancel"
|
||||
class="cancel-btn"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._setValue}>
|
||||
${this.hass.localize("ui.common.ok")}
|
||||
</ha-button>
|
||||
</ha-dialog>`;
|
||||
|
||||
<div class="bottom-actions">
|
||||
${this._params.canClear
|
||||
? html`<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._clear}
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.clear")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this._setToday}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.today")}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._setValue}>
|
||||
${this.hass.localize("ui.common.ok")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
@@ -108,11 +127,20 @@ export class HaDialogDatePicker extends LitElement {
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
--justify-action-buttons: space-between;
|
||||
}
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
gap: var(--ha-space-4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
app-datepicker {
|
||||
display: block;
|
||||
margin-inline: auto;
|
||||
--app-datepicker-accent-color: var(--primary-color);
|
||||
--app-datepicker-bg-color: transparent;
|
||||
--app-datepicker-color: var(--primary-text-color);
|
||||
@@ -129,11 +157,6 @@ export class HaDialogDatePicker extends LitElement {
|
||||
app-datepicker::part(body) {
|
||||
direction: ltr;
|
||||
}
|
||||
@media all and (min-width: 450px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 300px;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
app-datepicker {
|
||||
width: 100%;
|
||||
|
||||
@@ -2,7 +2,7 @@ import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-it
|
||||
import "@home-assistant/webawesome/dist/components/icon/icon";
|
||||
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
|
||||
import { css, type CSSResultGroup, html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
/**
|
||||
@@ -17,6 +17,8 @@ import "./ha-svg-icon";
|
||||
*/
|
||||
@customElement("ha-dropdown-item")
|
||||
export class HaDropdownItem extends DropdownItem {
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
protected renderCheckboxIcon() {
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
@@ -47,6 +49,13 @@ export class HaDropdownItem extends DropdownItem {
|
||||
:host([variant="danger"]) #icon ::slotted(*) {
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
}
|
||||
|
||||
:host([selected]) {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import type WaButton from "@home-assistant/webawesome/dist/components/button/button";
|
||||
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
||||
import { css, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HaDropdownItem } from "./ha-dropdown-item";
|
||||
|
||||
export type HaDropdownSelectEvent = CustomEvent<{ item: HaDropdownItem }>;
|
||||
/**
|
||||
* Event type for the ha-dropdown component when an item is selected.
|
||||
* @param T - The type of the value of the selected item.
|
||||
*/
|
||||
export type HaDropdownSelectEvent<T = string> = CustomEvent<{
|
||||
item: Omit<HaDropdownItem, "value"> & { value: T };
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Home Assistant dropdown component
|
||||
@@ -16,11 +23,37 @@ export type HaDropdownSelectEvent = CustomEvent<{ item: HaDropdownItem }>;
|
||||
*
|
||||
*/
|
||||
@customElement("ha-dropdown")
|
||||
// @ts-ignore Allow to set an alternative anchor element
|
||||
export class HaDropdown extends Dropdown {
|
||||
@property({ attribute: false }) dropdownTag = "ha-dropdown";
|
||||
|
||||
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
|
||||
|
||||
public get anchorElement(): HTMLButtonElement | WaButton | undefined {
|
||||
// @ts-ignore Allow to set an anchor element on popup
|
||||
return this.popup?.anchor as HTMLButtonElement | WaButton | undefined;
|
||||
}
|
||||
|
||||
public set anchorElement(element: HTMLButtonElement | WaButton | undefined) {
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
if (!this.popup) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
this.popup.anchor = element;
|
||||
}
|
||||
|
||||
/** Get the slotted trigger button, a <wa-button> or <button> element */
|
||||
// @ts-ignore Override parent method to be able to use alternative anchor
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override getTrigger(): HTMLButtonElement | WaButton | null {
|
||||
if (this.anchorElement) {
|
||||
return this.anchorElement;
|
||||
}
|
||||
// @ts-ignore fallback to default trigger slot if no anchorElement is set
|
||||
return super.getTrigger();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Dropdown.styles,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
import "./ha-button-toggle-group";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
|
||||
export interface HaDurationData {
|
||||
days?: number;
|
||||
@@ -152,7 +153,9 @@ class HaDurationInput extends LitElement {
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private _durationChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
|
||||
private _durationChanged(
|
||||
ev: ValueChangedEvent<TimeChangedEvent | undefined>
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value ? { ...ev.detail.value } : undefined;
|
||||
|
||||
|
||||
@@ -315,9 +315,13 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
ha-list {
|
||||
--mdc-list-item-meta-size: auto;
|
||||
--mdc-list-side-padding-right: 4px;
|
||||
--mdc-list-side-padding-right: var(--ha-space-1);
|
||||
--mdc-list-side-padding-left: var(--ha-space-4);
|
||||
--mdc-icon-button-size: 36px;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
ha-dropdown-item {
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
@@ -179,6 +179,9 @@ export class HaFilterDomains extends LitElement {
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -199,6 +199,9 @@ export class HaFilterIntegrations extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -164,6 +164,9 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -359,7 +359,7 @@ export class HaFloorPicker extends LitElement {
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.floor-picker.add_new_sugestion",
|
||||
"ui.components.floor-picker.add_new_suggestion",
|
||||
{
|
||||
name: searchString,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-md-divider";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.label-picker.add_new_sugestion",
|
||||
"ui.components.label-picker.add_new_suggestion",
|
||||
{
|
||||
name: searchString,
|
||||
}
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
import { Dialog } from "@material/web/dialog/internal/dialog";
|
||||
import { styles } from "@material/web/dialog/internal/dialog-styles";
|
||||
import {
|
||||
type DialogAnimation,
|
||||
DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||
DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||
} from "@material/web/dialog/internal/animations";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
// workaround to be able to overlay a dialog with another dialog
|
||||
Dialog.addInitializer(async (instance) => {
|
||||
await instance.updateComplete;
|
||||
|
||||
const dialogInstance = instance as HaMdDialog;
|
||||
|
||||
// @ts-expect-error dialog is private
|
||||
dialogInstance.dialog.prepend(dialogInstance.scrim);
|
||||
// @ts-expect-error scrim is private
|
||||
dialogInstance.scrim.style.inset = 0;
|
||||
// @ts-expect-error scrim is private
|
||||
dialogInstance.scrim.style.zIndex = 0;
|
||||
|
||||
const { getOpenAnimation, getCloseAnimation } = dialogInstance;
|
||||
dialogInstance.getOpenAnimation = () => {
|
||||
const animations = getOpenAnimation.call(this);
|
||||
animations.container = [
|
||||
...(animations.container ?? []),
|
||||
...(animations.dialog ?? []),
|
||||
];
|
||||
animations.dialog = [];
|
||||
return animations;
|
||||
};
|
||||
dialogInstance.getCloseAnimation = () => {
|
||||
const animations = getCloseAnimation.call(this);
|
||||
animations.container = [
|
||||
...(animations.container ?? []),
|
||||
...(animations.dialog ?? []),
|
||||
];
|
||||
animations.dialog = [];
|
||||
return animations;
|
||||
};
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
|
||||
|
||||
/**
|
||||
* Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
|
||||
*
|
||||
*/
|
||||
@customElement("ha-md-dialog")
|
||||
export class HaMdDialog extends Dialog {
|
||||
/**
|
||||
* When true the dialog will not close when the user presses the esc key or press out of the dialog.
|
||||
*/
|
||||
@property({ attribute: "disable-cancel-action", type: Boolean })
|
||||
public disableCancelAction = false;
|
||||
|
||||
private _polyfillDialogRegistered = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener("cancel", this._handleCancel);
|
||||
|
||||
if (typeof HTMLDialogElement !== "function") {
|
||||
this.addEventListener("open", this._handleOpen);
|
||||
|
||||
if (!DIALOG_POLYFILL) {
|
||||
DIALOG_POLYFILL = import("dialog-polyfill");
|
||||
}
|
||||
}
|
||||
|
||||
// if browser doesn't support animate API disable open/close animations
|
||||
if (this.animate === undefined) {
|
||||
this.quick = true;
|
||||
}
|
||||
|
||||
// if browser doesn't support animate API disable open/close animations
|
||||
if (this.animate === undefined) {
|
||||
this.quick = true;
|
||||
}
|
||||
}
|
||||
|
||||
// prevent open in older browsers and wait for polyfill to load
|
||||
private async _handleOpen(openEvent: Event) {
|
||||
openEvent.preventDefault();
|
||||
|
||||
if (this._polyfillDialogRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._polyfillDialogRegistered = true;
|
||||
this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
|
||||
const dialog = this.shadowRoot?.querySelector(
|
||||
"dialog"
|
||||
) as HTMLDialogElement;
|
||||
|
||||
const dialogPolyfill = await DIALOG_POLYFILL;
|
||||
dialogPolyfill.default.registerDialog(dialog);
|
||||
this.removeEventListener("open", this._handleOpen);
|
||||
|
||||
this.show();
|
||||
}
|
||||
|
||||
private async _loadPolyfillStylesheet(href) {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
link.onload = () => resolve();
|
||||
link.onerror = () =>
|
||||
reject(new Error(`Stylesheet failed to load: ${href}`));
|
||||
|
||||
this.shadowRoot?.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
private _handleCancel(closeEvent: Event) {
|
||||
if (this.disableCancelAction) {
|
||||
closeEvent.preventDefault();
|
||||
const dialogElement = this.shadowRoot?.querySelector("dialog .container");
|
||||
if (this.animate !== undefined) {
|
||||
dialogElement?.animate(
|
||||
[
|
||||
{
|
||||
transform: "rotate(-1deg)",
|
||||
"animation-timing-function": "ease-in",
|
||||
},
|
||||
{
|
||||
transform: "rotate(1.5deg)",
|
||||
"animation-timing-function": "ease-out",
|
||||
},
|
||||
{
|
||||
transform: "rotate(0deg)",
|
||||
"animation-timing-function": "ease-in",
|
||||
},
|
||||
],
|
||||
{
|
||||
duration: 200,
|
||||
iterations: 2,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-dialog-container-color: var(--card-background-color);
|
||||
--md-dialog-headline-color: var(--primary-text-color);
|
||||
--md-dialog-supporting-text-color: var(--primary-text-color);
|
||||
--md-sys-color-scrim: #000000;
|
||||
|
||||
--md-dialog-headline-weight: var(--ha-font-weight-normal);
|
||||
--md-dialog-headline-size: var(--ha-font-size-xl);
|
||||
--md-dialog-supporting-text-size: var(--ha-font-size-m);
|
||||
--md-dialog-supporting-text-line-height: var(--ha-line-height-normal);
|
||||
--md-divider-color: var(--divider-color);
|
||||
}
|
||||
|
||||
:host([type="alert"]) {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host(:not([type="alert"])) {
|
||||
min-width: var(--mdc-dialog-min-width, 100vw);
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
--md-dialog-container-shape: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: var(--safe-area-inset-top, 0);
|
||||
margin-bottom: var(--safe-area-inset-bottom, 0);
|
||||
margin-left: var(--safe-area-inset-left, 0);
|
||||
margin-right: var(--safe-area-inset-right, 0);
|
||||
}
|
||||
}
|
||||
|
||||
::slotted(ha-dialog-header[slot="headline"]) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
slot[name="actions"]::slotted(*) {
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.scroller {
|
||||
overflow: var(--dialog-content-overflow, auto);
|
||||
}
|
||||
|
||||
slot[name="content"]::slotted(*) {
|
||||
padding: var(--dialog-content-padding, var(--ha-space-6));
|
||||
}
|
||||
.scrim {
|
||||
z-index: 10; /* overlay navigation */
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
// by default the dialog open/close animation will be from/to the top
|
||||
// but if we have a special mobile dialog which is at the bottom of the screen, a from bottom animation can be used:
|
||||
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
|
||||
...DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||
dialog: [
|
||||
[
|
||||
// Dialog slide up
|
||||
[{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
|
||||
{ duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
|
||||
],
|
||||
],
|
||||
container: [
|
||||
[
|
||||
// Container fade in
|
||||
[{ opacity: 0 }, { opacity: 1 }],
|
||||
{ duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
|
||||
...DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||
dialog: [
|
||||
[
|
||||
// Dialog slide down
|
||||
[{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
|
||||
{ duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
|
||||
],
|
||||
],
|
||||
container: [
|
||||
[
|
||||
// Container fade out
|
||||
[{ opacity: "1" }, { opacity: "0" }],
|
||||
{ delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
export const getMobileOpenFromBottomAnimation = () => {
|
||||
const matches = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
|
||||
};
|
||||
|
||||
export const getMobileCloseToBottomAnimation = () => {
|
||||
const matches = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-dialog": HaMdDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Divider } from "@material/web/divider/internal/divider";
|
||||
import { styles } from "@material/web/divider/internal/divider-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-divider")
|
||||
export class HaMdDivider extends Divider {
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-divider-color: var(--divider-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-divider": HaMdDivider;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { MenuItemEl } from "@material/web/menu/internal/menuitem/menu-item";
|
||||
import { styles } from "@material/web/menu/internal/menuitem/menu-item-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-menu-item")
|
||||
export class HaMdMenuItem extends MenuItemEl {
|
||||
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
--md-sys-color-secondary-container: rgba(
|
||||
var(--rgb-primary-color),
|
||||
0.15
|
||||
);
|
||||
--md-sys-color-on-secondary-container: var(--text-primary-color);
|
||||
--mdc-icon-size: 16px;
|
||||
|
||||
--md-sys-color-on-primary-container: var(--primary-text-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
--md-menu-item-label-text-font: Roboto, sans-serif;
|
||||
}
|
||||
:host(.warning) {
|
||||
--md-menu-item-label-text-color: var(--error-color);
|
||||
--md-menu-item-leading-icon-color: var(--error-color);
|
||||
}
|
||||
::slotted([slot="headline"]) {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
:host([disabled]) {
|
||||
opacity: 1;
|
||||
--md-menu-item-label-text-color: var(--disabled-text-color);
|
||||
--md-menu-item-leading-icon-color: var(--disabled-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-menu-item": HaMdMenuItem;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Menu } from "@material/web/menu/internal/menu";
|
||||
import { styles } from "@material/web/menu/internal/menu-styles";
|
||||
import type { CloseMenuEvent } from "@material/web/menu/menu";
|
||||
import {
|
||||
CloseReason,
|
||||
KeydownCloseKey,
|
||||
} from "@material/web/menu/internal/controllers/shared";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { HaMdMenuItem } from "./ha-md-menu-item";
|
||||
|
||||
@customElement("ha-md-menu")
|
||||
export class HaMdMenu extends Menu {
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("close-menu", this._handleCloseMenu);
|
||||
}
|
||||
|
||||
private _handleCloseMenu(ev: CloseMenuEvent) {
|
||||
if (
|
||||
ev.detail.reason.kind === CloseReason.KEYDOWN &&
|
||||
ev.detail.reason.key === KeydownCloseKey.ESCAPE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-surface-container: var(--card-background-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-menu": HaMdMenu;
|
||||
}
|
||||
|
||||
interface HTMLElementEventMap {
|
||||
"close-menu": CloseMenuEvent;
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
} from "../data/supervisor/mounts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
|
||||
const _BACKUP_DATA_DISK_ = "/backup";
|
||||
|
||||
@@ -146,7 +146,7 @@ class HaMountPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _mountChanged(ev: CustomEvent<{ value: string }>) {
|
||||
private _mountChanged(ev: HaSelectSelectEvent) {
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (newValue !== this.value) {
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface PickerComboBoxItem {
|
||||
sorting_label?: string;
|
||||
icon_path?: string;
|
||||
icon?: string;
|
||||
isRelated?: boolean;
|
||||
}
|
||||
|
||||
export interface PickerComboBoxIndexSelectedDetail {
|
||||
|
||||
@@ -135,9 +135,7 @@ class HaQrScanner extends LitElement {
|
||||
(camera) => html`
|
||||
<ha-dropdown-item
|
||||
.value=${camera.id}
|
||||
class=${this._selectedCamera === camera.id
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${this._selectedCamera === camera.id}
|
||||
>
|
||||
${camera.label}
|
||||
</ha-dropdown-item>
|
||||
@@ -380,9 +378,6 @@ class HaQrScanner extends LitElement {
|
||||
color: white;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
}
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
+14
-16
@@ -17,6 +17,18 @@ export interface HaSelectOption {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event type for the ha-select component when an item is selected.
|
||||
* @param T - The type of the value of the selected item.
|
||||
* @param Clearable - Whether the select is clearable (allows undefined values).
|
||||
*/
|
||||
export type HaSelectSelectEvent<
|
||||
T = string,
|
||||
Clearable extends boolean = false,
|
||||
> = CustomEvent<{
|
||||
value: Clearable extends true ? T | undefined : T;
|
||||
}>;
|
||||
|
||||
@customElement("ha-select")
|
||||
export class HaSelect extends LitElement {
|
||||
@property({ type: Boolean }) public clearable = false;
|
||||
@@ -82,10 +94,8 @@ export class HaSelect extends LitElement {
|
||||
.disabled=${typeof option === "string"
|
||||
? false
|
||||
: (option.disabled ?? false)}
|
||||
class=${this.value ===
|
||||
(typeof option === "string" ? option : option.value)
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${this.value ===
|
||||
(typeof option === "string" ? option : option.value)}
|
||||
>
|
||||
${option.iconPath
|
||||
? html`<ha-svg-icon
|
||||
@@ -170,10 +180,6 @@ export class HaSelect extends LitElement {
|
||||
ha-picker-field.opened {
|
||||
--mdc-text-field-idle-line-color: var(--primary-color);
|
||||
}
|
||||
ha-dropdown-item.selected:hover {
|
||||
background-color: var(--ha-color-fill-primary-quiet-hover);
|
||||
}
|
||||
|
||||
ha-dropdown-item .content {
|
||||
display: flex;
|
||||
gap: var(--ha-space-1);
|
||||
@@ -188,14 +194,6 @@ export class HaSelect extends LitElement {
|
||||
ha-dropdown::part(menu) {
|
||||
min-width: var(--select-menu-width);
|
||||
}
|
||||
|
||||
:host ::slotted(ha-dropdown-item.selected),
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
declare global {
|
||||
|
||||
@@ -8,8 +8,8 @@ import { debounce } from "../common/util/debounce";
|
||||
import type { STTEngine } from "../data/stt";
|
||||
import { listSTTEngines } from "../data/stt";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@@ -141,7 +141,7 @@ export class HaSTTPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: CustomEvent<{ value: string }>): void {
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
} from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-generic-picker";
|
||||
@@ -403,7 +403,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _targetPicked(ev: CustomEvent<{ value: string }>) {
|
||||
private _targetPicked(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (value.startsWith(CREATE_ID)) {
|
||||
|
||||
@@ -3,8 +3,8 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
|
||||
const DEFAULT_THEME = "default";
|
||||
|
||||
@@ -63,7 +63,7 @@ export class HaThemePicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: CustomEvent<{ value: string }>): void {
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
if (!this.hass || ev.detail.value === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
|
||||
@customElement("ha-time-input")
|
||||
export class HaTimeInput extends LitElement {
|
||||
@@ -69,7 +70,7 @@ export class HaTimeInput extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _timeChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
|
||||
private _timeChanged(ev: ValueChangedEvent<TimeChangedEvent | undefined>) {
|
||||
ev.stopPropagation();
|
||||
const eventValue = ev.detail.value;
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import { debounce } from "../common/util/debounce";
|
||||
import type { TTSEngine } from "../data/tts";
|
||||
import { listTTSEngines } from "../data/tts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@@ -141,7 +141,7 @@ export class HaTTSPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: CustomEvent<{ value: string }>): void {
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
|
||||
@@ -6,8 +6,8 @@ import { debounce } from "../common/util/debounce";
|
||||
import type { TTSVoice } from "../data/tts";
|
||||
import { listTTSVoices } from "../data/tts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@@ -106,7 +106,7 @@ export class HaTTSVoicePicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: CustomEvent<{ value: string }>): void {
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@@ -197,22 +198,21 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
// temporary disabled because of issues with focus in iOS app, can be reenabled in 2026.2.0
|
||||
// if (isIosApp(this.hass)) {
|
||||
// const element = this.querySelector("[autofocus]");
|
||||
// if (element !== null) {
|
||||
// if (!element.id) {
|
||||
// element.id = "ha-wa-dialog-autofocus";
|
||||
// }
|
||||
// this.hass.auth.external!.fireMessage({
|
||||
// type: "focus_element",
|
||||
// payload: {
|
||||
// element_id: element.id,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
if (isIosApp(this.hass)) {
|
||||
const element = this.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-wa-dialog-autofocus";
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import { extractApiErrorMessage } from "../../data/hassio/common";
|
||||
@@ -19,8 +17,9 @@ import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-button";
|
||||
import "../ha-dialog";
|
||||
import "../ha-dialog-footer";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-wa-dialog";
|
||||
import "./ha-media-player-toggle";
|
||||
import type { JoinMediaPlayersDialogParams } from "./show-join-media-players-dialog";
|
||||
|
||||
@@ -38,8 +37,11 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: JoinMediaPlayersDialogParams): void {
|
||||
this._entityId = params.entityId;
|
||||
this._open = true;
|
||||
|
||||
const stateObj = this.hass.states[params.entityId] as
|
||||
| MediaPlayerEntity
|
||||
@@ -54,6 +56,11 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._entityId = undefined;
|
||||
this._selectedEntities = [];
|
||||
this._groupMembers = [];
|
||||
@@ -68,23 +75,18 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
}
|
||||
|
||||
const entityId = this._entityId;
|
||||
const stateObj = this.hass.states[entityId] as HassEntity | undefined;
|
||||
const name = (stateObj && computeStateName(stateObj)) || entityId;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
flexContent
|
||||
.heading=${name}
|
||||
@closed=${this.closeDialog}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
flexcontent
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-dialog-header show-border slot="heading">
|
||||
<ha-dialog-header show-border slot="header">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
data-dialog="close"
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button>
|
||||
<span slot="title"
|
||||
@@ -118,21 +120,23 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
></ha-media-player-toggle>`
|
||||
)}
|
||||
</div>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${!!this._submitting}
|
||||
slot="primaryAction"
|
||||
@click=${this._submit}
|
||||
>
|
||||
${this.hass.localize("ui.common.apply")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${!!this._submitting}
|
||||
slot="primaryAction"
|
||||
@click=${this._submit}
|
||||
>
|
||||
${this.hass.localize("ui.common.apply")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -217,6 +221,7 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: var(--ha-space-6);
|
||||
row-gap: var(--ha-space-4);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ import "../ha-icon-button";
|
||||
import "./hat-logbook-note";
|
||||
import type { NodeInfo } from "./hat-script-graph";
|
||||
import { traceTabStyles } from "./trace-tab-styles";
|
||||
import type { Trigger } from "../../data/automation";
|
||||
import { migrateAutomationTrigger } from "../../data/automation";
|
||||
|
||||
const TRACE_PATH_TABS = [
|
||||
"step_config",
|
||||
@@ -166,7 +168,9 @@ export class HaTracePathDetails extends LitElement {
|
||||
: selectedType === "trigger"
|
||||
? html`<h2>
|
||||
${describeTrigger(
|
||||
currentDetail,
|
||||
migrateAutomationTrigger({
|
||||
...currentDetail,
|
||||
}) as Trigger,
|
||||
this.hass,
|
||||
this._entityReg
|
||||
)}
|
||||
|
||||
@@ -32,12 +32,13 @@ export class VoiceAssistantBrandicon extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: inline;
|
||||
}
|
||||
.logo {
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
height: 24px;
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
+5
-20
@@ -14,6 +14,7 @@ import {
|
||||
import type { Collection, HassEntity } from "home-assistant-js-websocket";
|
||||
import { getCollection } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { normalizeValueBySIPrefix } from "../common/number/normalize-by-si-prefix";
|
||||
import {
|
||||
calcDate,
|
||||
calcDateProperty,
|
||||
@@ -1431,26 +1432,10 @@ export const getPowerFromState = (stateObj: HassEntity): number | undefined => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Normalize to watts (W) based on unit of measurement (case-sensitive)
|
||||
// Supported units: GW, kW, MW, mW, TW, W
|
||||
const unit = stateObj.attributes.unit_of_measurement;
|
||||
switch (unit) {
|
||||
case "W":
|
||||
return value;
|
||||
case "kW":
|
||||
return value * 1000;
|
||||
case "mW":
|
||||
return value / 1000;
|
||||
case "MW":
|
||||
return value * 1_000_000;
|
||||
case "GW":
|
||||
return value * 1_000_000_000;
|
||||
case "TW":
|
||||
return value * 1_000_000_000_000;
|
||||
default:
|
||||
// Assume value is in watts (W) if no unit or an unsupported unit is provided
|
||||
return value;
|
||||
}
|
||||
return normalizeValueBySIPrefix(
|
||||
value,
|
||||
stateObj.attributes.unit_of_measurement
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+2
-1
@@ -142,7 +142,7 @@ export const subscribeHistory = (
|
||||
);
|
||||
};
|
||||
|
||||
class HistoryStream {
|
||||
export class HistoryStream {
|
||||
hass: HomeAssistant;
|
||||
|
||||
hoursToShow?: number;
|
||||
@@ -221,6 +221,7 @@ class HistoryStream {
|
||||
// only expire the rest of the history as it ages.
|
||||
const lastExpiredState = expiredStates[expiredStates.length - 1];
|
||||
lastExpiredState.lu = purgeBeforePythonTime;
|
||||
delete lastExpiredState.lc;
|
||||
newHistory[entityId].unshift(lastExpiredState);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -41,12 +41,16 @@ export const enum TodoListEntityFeature {
|
||||
SET_DESCRIPTION_ON_ITEM = 64,
|
||||
}
|
||||
|
||||
export const getTodoLists = (hass: HomeAssistant): TodoList[] =>
|
||||
export const getTodoLists = (
|
||||
hass: HomeAssistant,
|
||||
includeHidden = true
|
||||
): TodoList[] =>
|
||||
Object.keys(hass.states)
|
||||
.filter(
|
||||
(entityId) =>
|
||||
computeDomain(entityId) === "todo" &&
|
||||
!isUnavailableState(hass.states[entityId].state)
|
||||
!isUnavailableState(hass.states[entityId].state) &&
|
||||
(includeHidden || hass.entities[entityId]?.hidden !== true)
|
||||
)
|
||||
.map((entityId) => ({
|
||||
...hass.states[entityId],
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button-toggle";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import {
|
||||
getMobileCloseToBottomAnimation,
|
||||
getMobileOpenFromBottomAnimation,
|
||||
} from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { LightColor, LightEntity } from "../../../../data/light";
|
||||
import {
|
||||
@@ -41,7 +36,7 @@ class DialogLightColorFavorite extends LitElement {
|
||||
|
||||
@state() private _modes: LightPickerMode[] = [];
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: LightColorFavoriteDialogParams
|
||||
@@ -50,10 +45,11 @@ class DialogLightColorFavorite extends LitElement {
|
||||
this._dialogParams = dialogParams;
|
||||
this._color = dialogParams.initialColor ?? this._computeCurrentColor();
|
||||
this._updateModes();
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._dialog?.close();
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _updateModes() {
|
||||
@@ -120,16 +116,8 @@ class DialogLightColorFavorite extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private async _cancel() {
|
||||
this._dialogParams?.cancel?.();
|
||||
}
|
||||
|
||||
private _cancelDialog() {
|
||||
this._cancel();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._dialogParams = undefined;
|
||||
this._entry = undefined;
|
||||
this._color = undefined;
|
||||
@@ -138,7 +126,7 @@ class DialogLightColorFavorite extends LitElement {
|
||||
|
||||
private async _save() {
|
||||
if (!this._color) {
|
||||
this._cancel();
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
this._dialogParams?.submit?.(this._color);
|
||||
@@ -159,83 +147,76 @@ class DialogLightColorFavorite extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-md-dialog
|
||||
open
|
||||
@cancel=${this._cancel}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
.headerTitle=${this._dialogParams?.title}
|
||||
@closed=${this._dialogClosed}
|
||||
aria-labelledby="dialog-light-color-favorite-title"
|
||||
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
|
||||
.getCloseAnimation=${getMobileCloseToBottomAnimation}
|
||||
>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this.closeDialog}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title" id="dialog-light-color-favorite-title"
|
||||
>${this._dialogParams?.title}</span
|
||||
>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
<div class="header">
|
||||
${this._modes.length > 1
|
||||
? html`
|
||||
<div class="modes">
|
||||
${this._modes.map(
|
||||
(value) => html`
|
||||
<ha-icon-button-toggle
|
||||
border-only
|
||||
.selected=${value === this._mode}
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
|
||||
)}
|
||||
.mode=${value}
|
||||
@click=${this._modeChanged}
|
||||
>
|
||||
<span
|
||||
class="wheel ${classMap({ [value]: true })}"
|
||||
></span>
|
||||
</ha-icon-button-toggle>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="content">
|
||||
${this._mode === "color_temp"
|
||||
? html`
|
||||
<light-color-temp-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-temp-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._mode === "color"
|
||||
? html`
|
||||
<light-color-rgb-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-rgb-picker>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="header">
|
||||
${this._modes.length > 1
|
||||
? html`
|
||||
<div class="modes">
|
||||
${this._modes.map(
|
||||
(value) => html`
|
||||
<ha-icon-button-toggle
|
||||
border-only
|
||||
.selected=${value === this._mode}
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
|
||||
)}
|
||||
.mode=${value}
|
||||
@click=${this._modeChanged}
|
||||
>
|
||||
<span
|
||||
class="wheel ${classMap({ [value]: true })}"
|
||||
></span>
|
||||
</ha-icon-button-toggle>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-button appearance="plain" @click=${this._cancelDialog}>
|
||||
<div class="content">
|
||||
${this._mode === "color_temp"
|
||||
? html`
|
||||
<light-color-temp-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-temp-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._mode === "color"
|
||||
? html`
|
||||
<light-color-rgb-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-rgb-picker>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._save} .disabled=${!this._color}
|
||||
>${this.hass.localize("ui.common.save")}</ha-button
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${!this._color}
|
||||
>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -243,23 +224,18 @@ class DialogLightColorFavorite extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-md-dialog {
|
||||
min-width: 420px; /* prevent width jumps when switching modes */
|
||||
max-height: min(
|
||||
ha-wa-dialog {
|
||||
--ha-dialog-width-md: 420px; /* prevent width jumps when switching modes */
|
||||
--ha-dialog-max-height: min(
|
||||
600px,
|
||||
100% - 48px
|
||||
); /* prevent scrolling on desktop */
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-md-dialog {
|
||||
min-width: 100%;
|
||||
min-height: auto;
|
||||
max-height: calc(100% - 100px);
|
||||
margin-bottom: 0;
|
||||
|
||||
--md-dialog-container-shape-start-start: 28px;
|
||||
--md-dialog-container-shape-start-end: 28px;
|
||||
ha-wa-dialog {
|
||||
--ha-dialog-width-md: 100vw;
|
||||
--ha-dialog-max-height: calc(100% - 100px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ export interface LightColorFavoriteDialogParams {
|
||||
title: string;
|
||||
initialColor?: LightColor;
|
||||
submit?: (color?: LightColor) => void;
|
||||
cancel?: () => void;
|
||||
}
|
||||
|
||||
export const loadLightColorFavoriteDialog = () =>
|
||||
@@ -18,7 +17,6 @@ export const showLightColorFavoriteDialog = (
|
||||
dialogParams: LightColorFavoriteDialogParams
|
||||
) =>
|
||||
new Promise<LightColor | null>((resolve) => {
|
||||
const origCancel = dialogParams.cancel;
|
||||
const origSubmit = dialogParams.submit;
|
||||
|
||||
fireEvent(element, "show-dialog", {
|
||||
@@ -26,12 +24,6 @@ export const showLightColorFavoriteDialog = (
|
||||
dialogImport: loadLightColorFavoriteDialog,
|
||||
dialogParams: {
|
||||
...dialogParams,
|
||||
cancel: () => {
|
||||
resolve(null);
|
||||
if (origCancel) {
|
||||
origCancel();
|
||||
}
|
||||
},
|
||||
submit: (color: LightColor) => {
|
||||
resolve(color);
|
||||
if (origSubmit) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-control-button";
|
||||
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-list-item";
|
||||
@@ -146,7 +147,7 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleToneChange(ev: CustomEvent<{ value: string }>) {
|
||||
private _handleToneChange(ev: HaSelectSelectEvent) {
|
||||
this._tone = ev.detail.value;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { setDateValue } from "../../../data/date";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
|
||||
@customElement("more-info-date")
|
||||
class MoreInfoDate extends LitElement {
|
||||
@@ -31,7 +31,7 @@ class MoreInfoDate extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
private _dateChanged(ev: ValueChangedEvent<string>): void {
|
||||
if (ev.detail.value) {
|
||||
setDateValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { setDateTimeValue } from "../../../data/datetime";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
|
||||
@customElement("more-info-datetime")
|
||||
class MoreInfoDatetime extends LitElement {
|
||||
@@ -45,7 +45,7 @@ class MoreInfoDatetime extends LitElement {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
private _timeChanged(ev: ValueChangedEvent<string>): void {
|
||||
if (ev.detail.value) {
|
||||
const dateObj = new Date(this.stateObj!.state);
|
||||
const newTime = ev.detail.value.split(":").map(Number);
|
||||
@@ -55,7 +55,7 @@ class MoreInfoDatetime extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
private _dateChanged(ev: ValueChangedEvent<string>): void {
|
||||
if (ev.detail.value) {
|
||||
const dateObj = new Date(this.stateObj!.state);
|
||||
const newDate = ev.detail.value.split("-").map(Number);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
setInputDateTimeValue,
|
||||
stateToIsoDateString,
|
||||
} from "../../../data/input_datetime";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
|
||||
@customElement("more-info-input_datetime")
|
||||
class MoreInfoInputDatetime extends LitElement {
|
||||
@@ -55,7 +55,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
private _timeChanged(ev: ValueChangedEvent<string>): void {
|
||||
setInputDateTimeValue(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id,
|
||||
@@ -66,7 +66,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
private _dateChanged(ev: ValueChangedEvent<string>): void {
|
||||
setInputDateTimeValue(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id,
|
||||
|
||||
@@ -202,21 +202,18 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-dropdown>
|
||||
return html`<ha-dropdown @wa-select=${this._handleSourceChange}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(`ui.card.media_player.source`)}
|
||||
.path=${mdiLoginVariant}
|
||||
@wa-select=${this._handleSourceChange}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${this.stateObj.attributes.source_list!.map(
|
||||
(source) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${source}
|
||||
class=${source === this.stateObj?.attributes.source
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${source === this.stateObj?.attributes.source}
|
||||
>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
@@ -251,9 +248,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
(soundMode) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${soundMode}
|
||||
class=${soundMode === this.stateObj?.attributes.sound_mode
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${soundMode === this.stateObj?.attributes.sound_mode}
|
||||
>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
@@ -679,13 +674,6 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
align-self: center;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
|
||||
private _handleClick(e: MouseEvent): void {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import type { HaSelectSelectEvent } from "../../../components/ha-select";
|
||||
import "../../../components/ha-select";
|
||||
import type { RemoteEntity } from "../../../data/remote";
|
||||
import { REMOTE_SUPPORT_ACTIVITY } from "../../../data/remote";
|
||||
@@ -43,7 +44,7 @@ class MoreInfoRemote extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleActivityChanged(ev: CustomEvent<{ value: string }>) {
|
||||
private _handleActivityChanged(ev: HaSelectSelectEvent) {
|
||||
const oldVal = this.stateObj!.attributes.current_activity;
|
||||
const newVal = ev.detail.value;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import { setTimeValue } from "../../../data/time";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
|
||||
@customElement("more-info-time")
|
||||
class MoreInfoTime extends LitElement {
|
||||
@@ -35,7 +35,7 @@ class MoreInfoTime extends LitElement {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
private _timeChanged(ev: ValueChangedEvent<string>): void {
|
||||
if (ev.detail.value) {
|
||||
setTimeValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import memoizeOne from "memoize-one";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
import type { HaSelectSelectEvent } from "../../../components/ha-select";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-select";
|
||||
@@ -285,7 +286,7 @@ class MoreInfoVacuum extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleFanSpeedChanged(ev: CustomEvent<{ value: string }>) {
|
||||
private _handleFanSpeedChanged(ev: HaSelectSelectEvent) {
|
||||
const oldVal = this.stateObj!.attributes.fan_speed;
|
||||
const newVal = ev.detail.value;
|
||||
|
||||
|
||||
@@ -313,113 +313,119 @@ class MoreInfoWeather extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<div class="section">
|
||||
${this.hass.localize("ui.card.weather.forecast")}:
|
||||
</div>
|
||||
${supportedForecasts?.length > 1
|
||||
? html`<ha-tab-group @wa-tab-show=${this._handleForecastTypeChanged}>
|
||||
${supportedForecasts.map(
|
||||
(forecastType) =>
|
||||
html`<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${forecastType}
|
||||
.active=${this._forecastType === forecastType}
|
||||
${supportedForecasts?.length
|
||||
? html`
|
||||
<div class="section">
|
||||
${this.hass.localize("ui.card.weather.forecast")}:
|
||||
</div>
|
||||
${supportedForecasts?.length > 1
|
||||
? html`<ha-tab-group
|
||||
@wa-tab-show=${this._handleForecastTypeChanged}
|
||||
>
|
||||
${this.hass!.localize(`ui.card.weather.${forecastType}`)}
|
||||
</ha-tab-group-tab>`
|
||||
)}
|
||||
</ha-tab-group>`
|
||||
: nothing}
|
||||
<div class="forecast">
|
||||
${forecast?.length
|
||||
? this._groupForecastByDay(forecast).map((dayForecast) => {
|
||||
const showDayHeader = hourly || dayNight;
|
||||
return html`
|
||||
<div class="forecast-day">
|
||||
${showDayHeader
|
||||
? html`<div class="forecast-day-header">
|
||||
${formatDateWeekdayShort(
|
||||
new Date(dayForecast[0].datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
${supportedForecasts.map(
|
||||
(forecastType) =>
|
||||
html`<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${forecastType}
|
||||
.active=${this._forecastType === forecastType}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.card.weather.${forecastType}`
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="forecast-day-content">
|
||||
${dayForecast.map((item) =>
|
||||
this._showValue(item.templow) ||
|
||||
this._showValue(item.temperature)
|
||||
? html`
|
||||
<div class="forecast-item">
|
||||
<div
|
||||
class="forecast-item-label ${showDayHeader
|
||||
? ""
|
||||
: "no-header"}"
|
||||
>
|
||||
${hourly
|
||||
? formatTime(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)
|
||||
: dayNight
|
||||
? html`<div class="daynight">
|
||||
${item.is_daytime !== false
|
||||
? this.hass!.localize(
|
||||
"ui.card.weather.day"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.card.weather.night"
|
||||
</ha-tab-group-tab>`
|
||||
)}
|
||||
</ha-tab-group>`
|
||||
: nothing}
|
||||
<div class="forecast">
|
||||
${forecast?.length
|
||||
? this._groupForecastByDay(forecast).map((dayForecast) => {
|
||||
const showDayHeader = hourly || dayNight;
|
||||
return html`
|
||||
<div class="forecast-day">
|
||||
${showDayHeader
|
||||
? html`<div class="forecast-day-header">
|
||||
${formatDateWeekdayShort(
|
||||
new Date(dayForecast[0].datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="forecast-day-content">
|
||||
${dayForecast.map((item) =>
|
||||
this._showValue(item.templow) ||
|
||||
this._showValue(item.temperature)
|
||||
? html`
|
||||
<div class="forecast-item">
|
||||
<div
|
||||
class="forecast-item-label ${showDayHeader
|
||||
? ""
|
||||
: "no-header"}"
|
||||
>
|
||||
${hourly
|
||||
? formatTime(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)
|
||||
: dayNight
|
||||
? html`<div class="daynight">
|
||||
${item.is_daytime !== false
|
||||
? this.hass!.localize(
|
||||
"ui.card.weather.day"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.card.weather.night"
|
||||
)}
|
||||
</div>`
|
||||
: formatDateWeekdayShort(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
</div>`
|
||||
: formatDateWeekdayShort(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
</div>
|
||||
${this._showValue(item.condition)
|
||||
? html`
|
||||
<div class="forecast-image-icon">
|
||||
${getWeatherStateIcon(
|
||||
item.condition!,
|
||||
this,
|
||||
!(
|
||||
item.is_daytime ||
|
||||
item.is_daytime === undefined
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? html`${formatNumber(
|
||||
item.temperature,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="templow">
|
||||
${this._showValue(item.templow)
|
||||
? html`${formatNumber(
|
||||
item.templow!,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
: html`<ha-spinner size="medium"></ha-spinner>`}
|
||||
</div>
|
||||
|
||||
${this._showValue(item.condition)
|
||||
? html`
|
||||
<div class="forecast-image-icon">
|
||||
${getWeatherStateIcon(
|
||||
item.condition!,
|
||||
this,
|
||||
!(
|
||||
item.is_daytime ||
|
||||
item.is_daytime === undefined
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? html`${formatNumber(
|
||||
item.temperature,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="templow">
|
||||
${this._showValue(item.templow)
|
||||
? html`${formatNumber(
|
||||
item.templow!,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
: html`<ha-spinner size="medium"></ha-spinner>`}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${this.stateObj.attributes.attribution
|
||||
? html`
|
||||
<div class="attribution">
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
type ActionCommandComboBoxItem,
|
||||
type NavigationComboBoxItem,
|
||||
} from "../../data/quick_bar";
|
||||
import type { RelatedResult } from "../../data/search";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
@@ -75,6 +76,8 @@ export class QuickBar extends LitElement {
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _relatedResult?: RelatedResult;
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
|
||||
private get _showEntityId() {
|
||||
@@ -100,6 +103,9 @@ export class QuickBar extends LitElement {
|
||||
this._initialize();
|
||||
this._selectedSection = params.mode;
|
||||
this._showHint = params.showHint ?? false;
|
||||
|
||||
this._relatedResult = params.contextItem ? params.related : undefined;
|
||||
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -417,6 +423,7 @@ export class QuickBar extends LitElement {
|
||||
this._selectedSection = section as QuickBarSection | undefined;
|
||||
return this._getItemsMemoized(
|
||||
this._configEntryLookup,
|
||||
this._relatedResult,
|
||||
searchString,
|
||||
this._selectedSection
|
||||
);
|
||||
@@ -425,10 +432,12 @@ export class QuickBar extends LitElement {
|
||||
private _getItemsMemoized = memoizeOne(
|
||||
(
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
relatedResult: RelatedResult | undefined,
|
||||
filter?: string,
|
||||
section?: QuickBarSection
|
||||
) => {
|
||||
const items: (string | PickerComboBoxItem)[] = [];
|
||||
const relatedIdSets = this._getRelatedIdSets(relatedResult);
|
||||
|
||||
if (!section || section === "navigate") {
|
||||
let navigateItems = this._generateNavigationCommandsMemoized(
|
||||
@@ -477,17 +486,29 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
if (!section || section === "entity") {
|
||||
let entityItems = this._getEntitiesMemoized(this.hass).sort(
|
||||
this._sortBySortingLabel
|
||||
);
|
||||
let entityItems = this._getEntitiesMemoized(this.hass);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.entities.size > 0) {
|
||||
entityItems = entityItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.entities.has(
|
||||
(item as EntityComboBoxItem).stateObj?.entity_id || ""
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
entityItems = this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
filter,
|
||||
entityComboBoxKeys
|
||||
) as EntityComboBoxItem[];
|
||||
entityItems = this._sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
filter,
|
||||
entityComboBoxKeys
|
||||
) as EntityComboBoxItem[]
|
||||
);
|
||||
} else {
|
||||
entityItems = this._sortRelatedByLabel(entityItems);
|
||||
}
|
||||
|
||||
if (!section && entityItems.length) {
|
||||
@@ -504,15 +525,25 @@ export class QuickBar extends LitElement {
|
||||
let deviceItems = this._getDevicesMemoized(
|
||||
this.hass,
|
||||
configEntryLookup
|
||||
).sort(this._sortBySortingLabel);
|
||||
);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.devices.size > 0) {
|
||||
deviceItems = deviceItems.map((item) => {
|
||||
const deviceId = item.id.split(SEPARATOR)[1];
|
||||
return {
|
||||
...item,
|
||||
isRelated: relatedIdSets.devices.has(deviceId || ""),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
deviceItems = this._filterGroup(
|
||||
"device",
|
||||
deviceItems,
|
||||
filter,
|
||||
deviceComboBoxKeys
|
||||
deviceItems = this._sortRelatedFirst(
|
||||
this._filterGroup("device", deviceItems, filter, deviceComboBoxKeys)
|
||||
);
|
||||
} else {
|
||||
deviceItems = this._sortRelatedByLabel(deviceItems);
|
||||
}
|
||||
|
||||
if (!section && deviceItems.length) {
|
||||
@@ -528,13 +559,23 @@ export class QuickBar extends LitElement {
|
||||
if (this.hass.user?.is_admin && (!section || section === "area")) {
|
||||
let areaItems = this._getAreasMemoized(this.hass);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.areas.size > 0) {
|
||||
areaItems = areaItems.map((item) => {
|
||||
const areaId = item.id.split(SEPARATOR)[1];
|
||||
return {
|
||||
...item,
|
||||
isRelated: relatedIdSets.areas.has(areaId || ""),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
areaItems = this._filterGroup(
|
||||
"area",
|
||||
areaItems,
|
||||
filter,
|
||||
areaComboBoxKeys
|
||||
areaItems = this._sortRelatedFirst(
|
||||
this._filterGroup("area", areaItems, filter, areaComboBoxKeys)
|
||||
);
|
||||
} else {
|
||||
areaItems = this._sortRelatedByLabel(areaItems);
|
||||
}
|
||||
|
||||
if (!section && areaItems.length) {
|
||||
@@ -551,6 +592,12 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getRelatedIdSets = memoizeOne((related?: RelatedResult) => ({
|
||||
entities: new Set(related?.entity || []),
|
||||
devices: new Set(related?.device || []),
|
||||
areas: new Set(related?.area || []),
|
||||
}));
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne((hass: HomeAssistant) =>
|
||||
getEntities(
|
||||
hass,
|
||||
@@ -654,6 +701,23 @@ export class QuickBar extends LitElement {
|
||||
this.hass.locale.language
|
||||
);
|
||||
|
||||
private _sortRelatedByLabel = (items: PickerComboBoxItem[]) =>
|
||||
[...items].sort((a, b) => {
|
||||
if (a.isRelated && !b.isRelated) return -1;
|
||||
if (!a.isRelated && b.isRelated) return 1;
|
||||
return this._sortBySortingLabel(a, b);
|
||||
});
|
||||
|
||||
private _sortRelatedFirst = (items: PickerComboBoxItem[]) =>
|
||||
[...items].sort((a, b) => {
|
||||
const aRelated = Boolean(a.isRelated);
|
||||
const bRelated = Boolean(b.isRelated);
|
||||
if (aRelated === bRelated) {
|
||||
return 0;
|
||||
}
|
||||
return aRelated ? -1 : 1;
|
||||
});
|
||||
|
||||
// #endregion data
|
||||
|
||||
// #region interaction
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ItemType, RelatedResult } from "../../data/search";
|
||||
import { closeDialog } from "../make-dialog-manager";
|
||||
|
||||
export type QuickBarSection =
|
||||
@@ -8,10 +9,17 @@ export type QuickBarSection =
|
||||
| "navigate"
|
||||
| "command";
|
||||
|
||||
export interface QuickBarContextItem {
|
||||
itemType: ItemType;
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
export interface QuickBarParams {
|
||||
entityFilter?: string;
|
||||
mode?: QuickBarSection;
|
||||
showHint?: boolean;
|
||||
contextItem?: QuickBarContextItem;
|
||||
related?: RelatedResult;
|
||||
}
|
||||
|
||||
export const loadQuickBar = () => import("./ha-quick-bar");
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
getPanelIconPath,
|
||||
getPanelTitle,
|
||||
} from "../../data/panel";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
|
||||
@customElement("dialog-edit-sidebar")
|
||||
@@ -206,7 +206,7 @@ class DialogEditSidebar extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _changed(ev: CustomEvent<{ value: DisplayValue }>): void {
|
||||
private _changed(ev: ValueChangedEvent<DisplayValue>): void {
|
||||
const { order = [], hidden = [] } = ev.detail.value;
|
||||
this._order = [...order];
|
||||
this._hidden = [...hidden];
|
||||
|
||||
@@ -129,11 +129,12 @@ export class CloudStepIntro extends LitElement {
|
||||
}
|
||||
.feature .logos {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
.feature .logos > * {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.round-icon {
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
|
||||
@@ -196,7 +196,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
(lang) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${lang.id}
|
||||
class=${this._language === lang.id ? "selected" : ""}
|
||||
.selected=${this._language === lang.id}
|
||||
>
|
||||
${lang.primary}
|
||||
</ha-dropdown-item>`
|
||||
@@ -407,13 +407,6 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
margin-inline-end: 12px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
ha-dropdown-item.selected {
|
||||
border: 1px solid var(--primary-color);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import type { HaSelectSelectEvent } from "../../components/ha-select";
|
||||
import {
|
||||
computeDeviceName,
|
||||
computeDeviceNameDisplay,
|
||||
@@ -231,7 +232,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
this._deviceName = ev.target.value;
|
||||
}
|
||||
|
||||
private async _wakeWordPicked(ev: CustomEvent<{ value: string }>) {
|
||||
private async _wakeWordPicked(ev: HaSelectSelectEvent) {
|
||||
const option = ev.detail.value;
|
||||
if (this.assistConfiguration) {
|
||||
this.assistConfiguration.active_wake_words = [option];
|
||||
@@ -239,7 +240,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
await setWakeWords(this.hass, this.assistEntityId!, [option]);
|
||||
}
|
||||
|
||||
private _pipelinePicked(ev: CustomEvent<{ value: string }>) {
|
||||
private _pipelinePicked(ev: HaSelectSelectEvent) {
|
||||
const stateObj = this.hass!.states[
|
||||
this.assistConfiguration!.pipeline_entity_id
|
||||
] as InputSelectEntity;
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { Panels } from "../types";
|
||||
export const demoPanels: Panels = {
|
||||
lovelace: {
|
||||
component_name: "lovelace",
|
||||
icon: null,
|
||||
title: null,
|
||||
icon: "mdi:view-dashboard",
|
||||
title: "demo",
|
||||
config: { mode: "storage" },
|
||||
url_path: "lovelace",
|
||||
},
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface MockHomeAssistant extends HomeAssistant {
|
||||
mockEvent(event);
|
||||
mockTheme(theme: Record<string, string> | null);
|
||||
formatEntityState(stateObj: HassEntity, state?: string): string;
|
||||
formatEntityStateToParts(stateObj: HassEntity, state?: string): ValuePart[];
|
||||
formatEntityAttributeValue(
|
||||
stateObj: HassEntity,
|
||||
attribute: string,
|
||||
@@ -117,6 +118,7 @@ export const provideHass = (
|
||||
async function updateFormatFunctions() {
|
||||
const {
|
||||
formatEntityState,
|
||||
formatEntityStateToParts,
|
||||
formatEntityAttributeName,
|
||||
formatEntityAttributeValue,
|
||||
formatEntityAttributeValueToParts,
|
||||
@@ -133,6 +135,7 @@ export const provideHass = (
|
||||
);
|
||||
hass().updateHass({
|
||||
formatEntityState,
|
||||
formatEntityStateToParts,
|
||||
formatEntityAttributeName,
|
||||
formatEntityAttributeValue,
|
||||
formatEntityAttributeValueToParts,
|
||||
@@ -256,6 +259,10 @@ export const provideHass = (
|
||||
darkMode: false,
|
||||
theme: "default",
|
||||
},
|
||||
selectedTheme: {
|
||||
theme: "default",
|
||||
dark: false,
|
||||
},
|
||||
panels: demoPanels,
|
||||
services: demoServices,
|
||||
user: {
|
||||
@@ -348,7 +355,7 @@ export const provideHass = (
|
||||
mockTheme(theme) {
|
||||
invalidateThemeCache();
|
||||
hass().updateHass({
|
||||
selectedTheme: { theme: theme ? "mock" : "default" },
|
||||
selectedTheme: { theme: theme ? "mock" : "default", dark: false },
|
||||
themes: {
|
||||
...hass().themes,
|
||||
themes: {
|
||||
@@ -361,7 +368,7 @@ export const provideHass = (
|
||||
document.documentElement,
|
||||
themes,
|
||||
selectedTheme!.theme,
|
||||
undefined,
|
||||
{ dark: false },
|
||||
true
|
||||
);
|
||||
},
|
||||
@@ -371,6 +378,12 @@ export const provideHass = (
|
||||
floors: {},
|
||||
formatEntityState: (stateObj, state) =>
|
||||
(state !== null ? state : stateObj.state) ?? "",
|
||||
formatEntityStateToParts: (stateObj, state) => [
|
||||
{
|
||||
type: "value",
|
||||
value: (state !== null ? state : stateObj.state) ?? "",
|
||||
},
|
||||
],
|
||||
formatEntityAttributeName: (_stateObj, attribute) => attribute,
|
||||
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||
value !== null ? value : (stateObj.attributes[attribute] ?? ""),
|
||||
|
||||
@@ -74,6 +74,8 @@ export class HAFullCalendar extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: "add-fab", type: Boolean }) public addFab = false;
|
||||
|
||||
@property({ attribute: false }) public events: CalendarEvent[] = [];
|
||||
|
||||
@property({ attribute: false }) public calendars: CalendarData[] = [];
|
||||
@@ -208,7 +210,7 @@ export class HAFullCalendar extends LitElement {
|
||||
: ""}
|
||||
|
||||
<div id="calendar"></div>
|
||||
${this._hasMutableCalendars
|
||||
${this.addFab && this._hasMutableCalendars
|
||||
? html`<ha-fab
|
||||
slot="fab"
|
||||
.label=${this.hass.localize("ui.components.calendar.event.add")}
|
||||
|
||||
@@ -193,6 +193,7 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
<ha-full-calendar
|
||||
add-fab
|
||||
.events=${this._events}
|
||||
.calendars=${this._calendars}
|
||||
.narrow=${this.narrow}
|
||||
@@ -330,6 +331,8 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
|
||||
ha-dropdown-item {
|
||||
padding-left: 32px;
|
||||
padding-inline-start: 32px;
|
||||
padding-inline-end: initial;
|
||||
--icon-primary-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
}
|
||||
|
||||
@@ -339,6 +342,8 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
|
||||
:host([mobile]) {
|
||||
padding-left: unset;
|
||||
padding-inline-start: unset;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
|
||||
@@ -11,6 +11,7 @@ import "../../components/chips/ha-chip-set";
|
||||
import "../../components/chips/ha-filter-chip";
|
||||
import "../../components/ha-date-input";
|
||||
import "../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../components/ha-select";
|
||||
import "../../components/ha-textfield";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type {
|
||||
@@ -311,8 +312,8 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
this._interval = (e.target! as any).value;
|
||||
}
|
||||
|
||||
private _onRepeatSelected(e: CustomEvent<{ value: string }>) {
|
||||
this._freq = e.detail.value as RepeatFrequency;
|
||||
private _onRepeatSelected(e: HaSelectSelectEvent<RepeatFrequency>) {
|
||||
this._freq = e.detail.value;
|
||||
|
||||
if (this._freq === "yearly") {
|
||||
this._interval = 1;
|
||||
@@ -323,7 +324,9 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _onMonthlyDetailSelected(e: CustomEvent<{ value: string }>) {
|
||||
private _onMonthlyDetailSelected(
|
||||
e: HaSelectSelectEvent<MonthlyRepeatItem["value"]>
|
||||
) {
|
||||
const selectedItem = this._monthlyRepeatItems.find(
|
||||
(item) => item.value === e.detail.value
|
||||
);
|
||||
@@ -346,8 +349,8 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
this.requestUpdate("_weekday");
|
||||
}
|
||||
|
||||
private _onEndSelected(e: CustomEvent<{ value: string }>) {
|
||||
const end = e.detail.value as RepeatEnd;
|
||||
private _onEndSelected(e: HaSelectSelectEvent<RepeatEnd>) {
|
||||
const end = e.detail.value;
|
||||
if (end === this._end) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
|
||||
import type {
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
@@ -118,14 +119,14 @@ class SupervisorAppAudio extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _setInputDevice(ev: CustomEvent<{ value: string }>): void {
|
||||
private _setInputDevice(ev: HaSelectSelectEvent): void {
|
||||
const device = ev.detail.value;
|
||||
this._selectedInput = device;
|
||||
this._selectedInput = device ?? null;
|
||||
}
|
||||
|
||||
private _setOutputDevice(ev: CustomEvent<{ value: string }>): void {
|
||||
private _setOutputDevice(ev: HaSelectSelectEvent): void {
|
||||
const device = ev.detail.value;
|
||||
this._selectedOutput = device;
|
||||
this._selectedOutput = device ?? null;
|
||||
}
|
||||
|
||||
private async _addonChanged(): Promise<void> {
|
||||
|
||||
@@ -63,6 +63,7 @@ class SupervisorAppDocumentationDashboard extends LitElement {
|
||||
margin: auto;
|
||||
padding: var(--ha-space-2);
|
||||
max-width: 1024px;
|
||||
direction: ltr;
|
||||
}
|
||||
ha-markdown {
|
||||
padding: var(--ha-space-4);
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import { CONDITION_ICONS } from "../../../../../components/ha-condition-icon";
|
||||
import "../../../../../components/ha-dropdown-item";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
|
||||
import {
|
||||
DYNAMIC_PREFIX,
|
||||
getValueFromDynamic,
|
||||
@@ -101,7 +102,7 @@ export class HaConditionAction
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dropdown-item .value=${opt} class=${selected ? "selected" : ""}>
|
||||
<ha-dropdown-item .value=${opt} .selected=${selected}>
|
||||
<ha-condition-icon
|
||||
.hass=${this.hass}
|
||||
slot="icon"
|
||||
@@ -200,7 +201,7 @@ export class HaConditionAction
|
||||
});
|
||||
}
|
||||
|
||||
private _typeChanged(ev: CustomEvent<{ value: string }>) {
|
||||
private _typeChanged(ev: HaSelectSelectEvent) {
|
||||
const type = ev.detail.value;
|
||||
|
||||
if (!type) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import "../../../../../components/ha-duration-input";
|
||||
import "../../../../../components/ha-formfield";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import type { WaitForTriggerAction } from "../../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
|
||||
import "../../trigger/ha-automation-trigger";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
import { handleChangeEvent } from "../ha-automation-action-row";
|
||||
@@ -78,7 +78,7 @@ export class HaWaitForTriggerAction
|
||||
`;
|
||||
}
|
||||
|
||||
private _timeoutChanged(ev: CustomEvent<{ value: TimeChangedEvent }>): void {
|
||||
private _timeoutChanged(ev: ValueChangedEvent<TimeChangedEvent>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
fireEvent(this, "value-changed", {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAppleKeyboardCommand,
|
||||
@@ -42,7 +43,6 @@ import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-button-prev";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
|
||||
@@ -114,7 +114,7 @@ import {
|
||||
} from "../../../data/trigger";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "./add-automation-element/ha-automation-add-from-target";
|
||||
@@ -657,10 +657,7 @@ class DialogAddAutomationElement
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</ha-md-list-item>
|
||||
<ha-md-divider
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></ha-md-divider>`
|
||||
<wa-divider></wa-divider>`
|
||||
: nothing}
|
||||
${collections.map(
|
||||
(collection, index) => html`
|
||||
@@ -1752,7 +1749,7 @@ class DialogAddAutomationElement
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _selected(ev: CustomEvent<{ value: string }>) {
|
||||
private _selected(ev: ValueChangedEvent<string>) {
|
||||
let target: HassServiceTarget | undefined;
|
||||
if (
|
||||
this._tab === "targets" &&
|
||||
@@ -1766,7 +1763,7 @@ class DialogAddAutomationElement
|
||||
}
|
||||
|
||||
private _handleTargetSelected = (
|
||||
ev: CustomEvent<{ value: SingleHassServiceTarget }>
|
||||
ev: ValueChangedEvent<SingleHassServiceTarget>
|
||||
) => {
|
||||
this._targetItems = undefined;
|
||||
this._loadItemsError = false;
|
||||
@@ -2177,8 +2174,8 @@ class DialogAddAutomationElement
|
||||
width: var(--ha-space-6);
|
||||
}
|
||||
|
||||
ha-md-list-item.paste {
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
wa-divider {
|
||||
--spacing: 0;
|
||||
}
|
||||
|
||||
ha-svg-icon.plus {
|
||||
|
||||
@@ -77,7 +77,12 @@ import "../../../layouts/hass-subpage";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { Entries, HomeAssistant, Route } from "../../../types";
|
||||
import type {
|
||||
Entries,
|
||||
HomeAssistant,
|
||||
Route,
|
||||
ValueChangedEvent,
|
||||
} from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
@@ -753,7 +758,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent<{ value: AutomationConfig }>) {
|
||||
private _valueChanged(ev: ValueChangedEvent<AutomationConfig>) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this._config) {
|
||||
|
||||
@@ -47,6 +47,10 @@ import type {
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type {
|
||||
HaDropdown,
|
||||
HaDropdownSelectEvent,
|
||||
} from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-blueprints";
|
||||
@@ -57,10 +61,6 @@ import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-voice-assistants";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-menu";
|
||||
import type { HaMdMenu } from "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
@@ -84,9 +84,9 @@ import { fullEntitiesContext } from "../../../data/context";
|
||||
import type { DataTableFilters } from "../../../data/data_table_filters";
|
||||
import {
|
||||
deserializeFilters,
|
||||
serializeFilters,
|
||||
isFilterUsed,
|
||||
isRelatedItemsFilterUsed,
|
||||
serializeFilters,
|
||||
} from "../../../data/data_table_filters";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type {
|
||||
@@ -111,16 +111,16 @@ import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route, ServiceCallResponse } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
|
||||
import {
|
||||
getEntityIdHiddenTableColumn,
|
||||
getAreaTableColumn,
|
||||
getCategoryTableColumn,
|
||||
getLabelsTableColumn,
|
||||
getTriggeredAtTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import {
|
||||
getAreaTableColumn,
|
||||
getCategoryTableColumn,
|
||||
getEntityIdHiddenTableColumn,
|
||||
getLabelsTableColumn,
|
||||
getTriggeredAtTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
@@ -129,7 +129,6 @@ import {
|
||||
} from "../voice-assistants/expose/assistants-table-column";
|
||||
import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants";
|
||||
import { showNewAutomationDialog } from "./show-dialog-new-automation";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
type AutomationItem = AutomationEntity & {
|
||||
name: string;
|
||||
@@ -223,7 +222,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu!: HaMdMenu;
|
||||
@query("#overflow-menu") private _overflowMenu!: HaDropdown;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
@@ -233,6 +232,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
return getAvailableAssistants(this.cloudStatus, this.hass);
|
||||
}
|
||||
|
||||
private _openingOverflow = false;
|
||||
|
||||
private _automations = memoizeOne(
|
||||
(
|
||||
automations: AutomationEntity[],
|
||||
@@ -371,16 +372,27 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _showOverflowMenu = (ev) => {
|
||||
if (
|
||||
this._overflowMenu.open &&
|
||||
ev.target === this._overflowMenu.anchorElement
|
||||
) {
|
||||
this._overflowMenu.close();
|
||||
if (this._overflowMenu.anchorElement === ev.target) {
|
||||
this._overflowMenu.anchorElement = undefined;
|
||||
return;
|
||||
}
|
||||
this._overflowAutomation = ev.target.automation;
|
||||
this._openingOverflow = true;
|
||||
this._overflowMenu.anchorElement = ev.target;
|
||||
this._overflowMenu.show();
|
||||
this._overflowAutomation = ev.target.automation;
|
||||
this._overflowMenu.open = true;
|
||||
};
|
||||
|
||||
private _overflowMenuOpened = () => {
|
||||
this._openingOverflow = false;
|
||||
};
|
||||
|
||||
private _overflowMenuClosed = () => {
|
||||
// changing the anchorElement triggers a close event, ignore it
|
||||
if (this._openingOverflow) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overflowMenu.anchorElement = undefined;
|
||||
};
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
@@ -697,74 +709,58 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-md-menu id="overflow-menu" positioning="fixed">
|
||||
<ha-md-menu-item .clickAction=${this._showInfo}>
|
||||
<ha-svg-icon
|
||||
.path=${mdiInformationOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-dropdown
|
||||
id="overflow-menu"
|
||||
@wa-select=${this._handleOverflowAction}
|
||||
@wa-after-show=${this._overflowMenuOpened}
|
||||
@wa-after-hide=${this._overflowMenuClosed}
|
||||
>
|
||||
<ha-dropdown-item value="show_info">
|
||||
<ha-svg-icon .path=${mdiInformationOutline} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._showSettings}>
|
||||
<ha-svg-icon .path=${mdiCog} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_settings"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._editCategory}>
|
||||
<ha-svg-icon .path=${mdiTag} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._runActions}>
|
||||
<ha-svg-icon .path=${mdiPlay} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.editor.run")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._showTrace}>
|
||||
<ha-svg-icon .path=${mdiTransitConnection} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.show_trace"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._duplicate}>
|
||||
<ha-svg-icon .path=${mdiContentDuplicate} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._toggle}>
|
||||
<ha-dropdown-item value="show_settings">
|
||||
<ha-svg-icon .path=${mdiCog} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_settings"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="edit_category">
|
||||
<ha-svg-icon .path=${mdiTag} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="run_actions">
|
||||
<ha-svg-icon .path=${mdiPlay} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.run")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="show_trace">
|
||||
<ha-svg-icon .path=${mdiTransitConnection} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.show_trace")}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item value="duplicate">
|
||||
<ha-svg-icon .path=${mdiContentDuplicate} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="toggle">
|
||||
<ha-svg-icon
|
||||
.path=${this._overflowAutomation?.state === "off"
|
||||
? mdiToggleSwitch
|
||||
: mdiToggleSwitchOffOutline}
|
||||
slot="start"
|
||||
slot="icon"
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this._overflowAutomation?.state === "off"
|
||||
? this.hass.localize("ui.panel.config.automation.editor.enable")
|
||||
: this.hass.localize("ui.panel.config.automation.editor.disable")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._deleteConfirm} class="warning">
|
||||
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.picker.delete")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-menu>
|
||||
${this._overflowAutomation?.state === "off"
|
||||
? this.hass.localize("ui.panel.config.automation.editor.enable")
|
||||
: this.hass.localize("ui.panel.config.automation.editor.disable")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="delete" variant="danger">
|
||||
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.picker.delete")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -901,33 +897,59 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _showInfo = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
fireEvent(this, "hass-more-info", { entityId: automation.entity_id });
|
||||
private _handleOverflowAction = (ev: HaDropdownSelectEvent) => {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (!action || !this._overflowAutomation) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "show_info":
|
||||
this._showInfo(this._overflowAutomation);
|
||||
break;
|
||||
case "show_settings":
|
||||
this._showSettings(this._overflowAutomation);
|
||||
break;
|
||||
case "edit_category":
|
||||
this._editCategory(this._overflowAutomation);
|
||||
break;
|
||||
case "run_actions":
|
||||
this._runActions(this._overflowAutomation);
|
||||
break;
|
||||
case "show_trace":
|
||||
this._showTrace(this._overflowAutomation);
|
||||
break;
|
||||
case "toggle":
|
||||
this._toggle(this._overflowAutomation);
|
||||
break;
|
||||
case "delete":
|
||||
this._deleteConfirm(this._overflowAutomation);
|
||||
break;
|
||||
case "duplicate":
|
||||
this._duplicate(this._overflowAutomation);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private _showSettings = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
private _showInfo = (automation: AutomationItem) => {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: automation.entity_id,
|
||||
});
|
||||
};
|
||||
|
||||
private _showSettings = (automation: AutomationItem) => {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: automation.entity_id,
|
||||
view: "settings",
|
||||
});
|
||||
};
|
||||
|
||||
private _runActions = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _runActions = (automation: AutomationItem) => {
|
||||
triggerAutomationActions(this.hass, automation.entity_id);
|
||||
};
|
||||
|
||||
private _editCategory = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _editCategory = (automation: AutomationItem) => {
|
||||
const entityReg = this._entityReg.find(
|
||||
(reg) => reg.entity_id === automation.entity_id
|
||||
);
|
||||
@@ -948,10 +970,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private _showTrace = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _showTrace = (automation: AutomationItem) => {
|
||||
if (!automation.attributes.id) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
@@ -965,20 +984,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
};
|
||||
|
||||
private _toggle = async (item: HaMdMenuItem): Promise<void> => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _toggle = async (automation: AutomationItem): Promise<void> => {
|
||||
const service = automation.state === "off" ? "turn_on" : "turn_off";
|
||||
await this.hass.callService("automation", service, {
|
||||
entity_id: automation.entity_id,
|
||||
});
|
||||
};
|
||||
|
||||
private _deleteConfirm = async (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _deleteConfirm = async (automation: AutomationItem) => {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.picker.delete_confirm_title"
|
||||
@@ -994,9 +1007,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private async _delete(automation) {
|
||||
private async _delete(automation: AutomationItem) {
|
||||
try {
|
||||
await deleteAutomation(this.hass, automation.attributes.id);
|
||||
await deleteAutomation(this.hass, automation.attributes.id!);
|
||||
this._selected = this._selected.filter(
|
||||
(entityId) => entityId !== automation.entity_id
|
||||
);
|
||||
@@ -1015,14 +1028,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _duplicate = async (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _duplicate = async (automation: AutomationItem) => {
|
||||
try {
|
||||
const config = await fetchAutomationFileConfig(
|
||||
this.hass,
|
||||
automation.attributes.id
|
||||
automation.attributes.id!
|
||||
);
|
||||
duplicateAutomation(config);
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
import { configEntriesContext } from "../../../data/context";
|
||||
import { getActionType, type Action } from "../../../data/script";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "./action/ha-automation-action";
|
||||
@@ -383,7 +383,7 @@ export class HaManualAutomationEditor extends SubscribeMixin(LitElement) {
|
||||
this._sidebarElement?.focus();
|
||||
}
|
||||
|
||||
private _sidebarConfigChanged(ev: CustomEvent<{ value: SidebarConfig }>) {
|
||||
private _sidebarConfigChanged(ev: ValueChangedEvent<SidebarConfig>) {
|
||||
ev.stopPropagation();
|
||||
if (!this._sidebarConfig) {
|
||||
return;
|
||||
@@ -438,7 +438,6 @@ export class HaManualAutomationEditor extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _saveAutomation() {
|
||||
this.triggerCloseSidebar();
|
||||
fireEvent(this, "save-automation");
|
||||
}
|
||||
|
||||
|
||||
@@ -40,9 +40,6 @@ export const rowStyles = css`
|
||||
.warning ul {
|
||||
margin: 4px 0;
|
||||
}
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
ha-tooltip {
|
||||
cursor: default;
|
||||
}
|
||||
@@ -272,7 +269,4 @@ export const overflowStyles = css`
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
ha-md-menu-item {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
|
||||
import type { TagTrigger } from "../../../../../data/automation";
|
||||
import type { Tag } from "../../../../../data/tag";
|
||||
import { fetchTags } from "../../../../../data/tag";
|
||||
@@ -60,7 +61,7 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _tagChanged(ev: CustomEvent<{ value: string }>) {
|
||||
private _tagChanged(ev: HaSelectSelectEvent) {
|
||||
if (
|
||||
!ev.detail.value ||
|
||||
!this._tags ||
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
sortWeekdays,
|
||||
} from "../../../../../data/backup";
|
||||
import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
|
||||
import { documentationUrl } from "../../../../../util/documentation-url";
|
||||
import "./ha-backup-config-retention";
|
||||
|
||||
@@ -405,7 +405,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _retentionChanged(ev: CustomEvent<{ value: Retention }>) {
|
||||
private _retentionChanged(ev: ValueChangedEvent<Retention>) {
|
||||
ev.stopPropagation();
|
||||
const retention = ev.detail.value;
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-password-field";
|
||||
@@ -86,14 +85,12 @@ const RECOMMENDED_CONFIG: BackupConfig = {
|
||||
class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _opened = false;
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _step?: Step;
|
||||
|
||||
@state() private _params?: BackupOnboardingDialogParams;
|
||||
|
||||
@query("ha-md-dialog") private _dialog!: HaMdDialog;
|
||||
|
||||
@state() private _config?: BackupConfig;
|
||||
|
||||
public showDialog(params: BackupOnboardingDialogParams): void {
|
||||
@@ -115,21 +112,23 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
};
|
||||
}
|
||||
|
||||
this._opened = true;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
if (this._params!.cancel) {
|
||||
this._params!.cancel();
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
if (this._params?.cancel) {
|
||||
this._params.cancel();
|
||||
}
|
||||
if (this._opened) {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
this._opened = false;
|
||||
this._step = undefined;
|
||||
this._config = undefined;
|
||||
this._params = undefined;
|
||||
return true;
|
||||
this._open = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private get _firstStep(): Step {
|
||||
@@ -168,7 +167,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
try {
|
||||
await this._save(true);
|
||||
this._params?.submit!(true);
|
||||
this._dialog.close();
|
||||
this.closeDialog();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
@@ -202,7 +201,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened || !this._params || !this._step) {
|
||||
if (!this._params || !this._step) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
@@ -210,33 +209,37 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
const isFirstStep = this._step === this._firstStep;
|
||||
|
||||
return html`
|
||||
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
|
||||
<ha-dialog-header slot="headline">
|
||||
${isFirstStep
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-prev
|
||||
slot="navigationIcon"
|
||||
@click=${this._previousStep}
|
||||
></ha-icon-button-prev>
|
||||
`}
|
||||
|
||||
<span slot="title">${this._stepTitle}</span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">${this._renderStepContent()}</div>
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._stepTitle}
|
||||
width="medium"
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${isFirstStep
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="headerNavigationIcon"
|
||||
data-dialog="close"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-prev
|
||||
slot="headerNavigationIcon"
|
||||
@click=${this._previousStep}
|
||||
></ha-icon-button-prev>
|
||||
`}
|
||||
<div>${this._renderStepContent()}</div>
|
||||
${!FULL_DIALOG_STEPS.has(this._step)
|
||||
? html`
|
||||
<div slot="actions">
|
||||
<ha-dialog-footer slot="footer">
|
||||
${isLastStep
|
||||
? html`
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._done}
|
||||
.disabled=${!this._isStepValid()}
|
||||
>
|
||||
@@ -247,16 +250,17 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
`
|
||||
: html`
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._nextStep}
|
||||
.disabled=${!this._isStepValid()}
|
||||
>
|
||||
${this.hass.localize("ui.common.next")}
|
||||
</ha-button>
|
||||
`}
|
||||
</div>
|
||||
</ha-dialog-footer>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-dialog>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -540,11 +544,9 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-md-dialog {
|
||||
width: 90vw;
|
||||
max-width: 560px;
|
||||
--dialog-content-padding: 8px 24px;
|
||||
max-height: min(605px, 100% - 48px);
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
|
||||
--ha-dialog-max-height: min(605px, 100% - 48px);
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
@@ -557,14 +559,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
margin-left: -24px;
|
||||
margin-right: -24px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-md-dialog {
|
||||
max-width: none;
|
||||
}
|
||||
div[slot="content"] {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
@@ -36,13 +37,20 @@ class LocalBackupLocationDialog extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: LocalBackupLocationDialogParams
|
||||
): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._data = undefined;
|
||||
this._error = undefined;
|
||||
this._waiting = undefined;
|
||||
@@ -55,17 +63,13 @@ class LocalBackupLocationDialog extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize(
|
||||
`ui.panel.config.backup.dialogs.local_backup_location.title`
|
||||
)
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
`ui.panel.config.backup.dialogs.local_backup_location.title`
|
||||
)}
|
||||
@closed=${this.closeDialog}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
@@ -77,34 +81,35 @@ class LocalBackupLocationDialog extends LitElement {
|
||||
)}
|
||||
</p>
|
||||
<ha-form
|
||||
autofocus
|
||||
.hass=${this.hass}
|
||||
.data=${this._data}
|
||||
.schema=${SCHEMA}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
dialogInitialFocus
|
||||
></ha-form>
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.backup.dialogs.local_backup_location.note`
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${this._waiting || !this._data}
|
||||
slot="primaryAction"
|
||||
@click=${this._changeMount}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${this._waiting || !this._data}
|
||||
slot="primaryAction"
|
||||
@click=${this._changeMount}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -143,9 +148,6 @@ class LocalBackupLocationDialog extends LitElement {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 500px;
|
||||
}
|
||||
ha-form {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-password-field";
|
||||
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import type { RestoreBackupParams } from "../../../../data/backup";
|
||||
import {
|
||||
fetchBackupConfig,
|
||||
@@ -54,6 +51,8 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
|
||||
@state() private _formData?: FormData;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _backupEncryptionKey?: string;
|
||||
|
||||
@state() private _userPassword?: string;
|
||||
@@ -68,8 +67,6 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
|
||||
@state() private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public async showDialog(params: RestoreBackupDialogParams) {
|
||||
this._params = params;
|
||||
|
||||
@@ -94,10 +91,12 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
} else {
|
||||
this._step = STEPS[0];
|
||||
}
|
||||
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialog?.close();
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -112,6 +111,7 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
this._stage = undefined;
|
||||
this._step = undefined;
|
||||
this._unsubscribe();
|
||||
this._open = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -136,17 +136,14 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
<span slot="title" .title=${dialogTitle}>${dialogTitle}</span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content" class="content">
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${dialogTitle}
|
||||
width="medium"
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div class="content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: this._step === "confirm"
|
||||
@@ -155,18 +152,18 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
? this._renderEncryption()
|
||||
: this._renderProgress()}
|
||||
</div>
|
||||
<div slot="actions">
|
||||
${this._error
|
||||
? html`
|
||||
<ha-button @click=${this.closeDialog}>
|
||||
${this._error
|
||||
? html`
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</ha-button>
|
||||
`
|
||||
: this._step === "confirm" || this._step === "encryption"
|
||||
? this._renderConfirmActions()
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
</ha-dialog-footer>
|
||||
`
|
||||
: this._step === "confirm" || this._step === "encryption"
|
||||
? this._renderConfirmActions()
|
||||
: nothing}
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -216,6 +213,7 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
${this._renderEncryptionIntro()}
|
||||
|
||||
<ha-password-field
|
||||
autofocus
|
||||
@input=${this._passwordChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.restore.encryption.input_label"
|
||||
@@ -227,14 +225,24 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
|
||||
private _renderConfirmActions() {
|
||||
return html`
|
||||
<ha-button appearance="plain" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._restoreBackup} variant="danger">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.restore.actions.restore"
|
||||
)}
|
||||
</ha-button>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._restoreBackup}
|
||||
variant="danger"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.restore.actions.restore"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -365,10 +373,6 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-md-dialog {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
.content p {
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
|
||||
import { mdiContentCopy, mdiDownload } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-password-field";
|
||||
@@ -31,40 +29,40 @@ type Step = (typeof STEPS)[number];
|
||||
class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _opened = false;
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _step?: Step;
|
||||
|
||||
@state() private _params?: SetBackupEncryptionKeyDialogParams;
|
||||
|
||||
@query("ha-md-dialog") private _dialog!: HaMdDialog;
|
||||
|
||||
@state() private _newEncryptionKey?: string;
|
||||
|
||||
public showDialog(params: SetBackupEncryptionKeyDialogParams): void {
|
||||
this._params = params;
|
||||
this._step = STEPS[0];
|
||||
this._opened = true;
|
||||
this._open = true;
|
||||
this._newEncryptionKey = generateEncryptionKey();
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
if (this._params!.cancel) {
|
||||
this._params!.cancel();
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
if (this._params?.cancel) {
|
||||
this._params.cancel();
|
||||
}
|
||||
if (this._opened) {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
this._opened = false;
|
||||
this._step = undefined;
|
||||
this._params = undefined;
|
||||
this._newEncryptionKey = undefined;
|
||||
return true;
|
||||
this._open = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _done() {
|
||||
this._params?.submit!(true);
|
||||
this._dialog.close();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _nextStep() {
|
||||
@@ -76,7 +74,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened || !this._params) {
|
||||
if (!this._params || !this._step) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
@@ -88,21 +86,20 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
<span slot="title">${dialogTitle}</span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">${this._renderStepContent()}</div>
|
||||
<div slot="actions">
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${dialogTitle}
|
||||
width="medium"
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._renderStepContent()}
|
||||
<ha-dialog-footer slot="footer">
|
||||
${this._step === "key"
|
||||
? html`
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._submit}
|
||||
.disabled=${!this._newEncryptionKey}
|
||||
>
|
||||
@@ -112,14 +109,14 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
<ha-button @click=${this._done}>
|
||||
<ha-button slot="primaryAction" @click=${this._done}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.set_encryption_key.actions.done"
|
||||
)}
|
||||
</ha-button>
|
||||
`}
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -213,10 +210,8 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-md-dialog {
|
||||
width: 90vw;
|
||||
max-width: 560px;
|
||||
--dialog-content-padding: 8px 24px;
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
@@ -247,14 +242,6 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
flex: none;
|
||||
margin: -16px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-md-dialog {
|
||||
max-width: none;
|
||||
}
|
||||
div[slot="content"] {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user