Compare commits

..

1 Commits

Author SHA1 Message Date
Aidan Timson fc27e362a6 Migrate config-users dialog(s) to wa 2026-02-05 15:41:05 +00:00
142 changed files with 2280 additions and 3547 deletions
+3 -3
View File
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
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@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
# ️ 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@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
+1 -1
View File
@@ -1 +1 @@
24.13.1
24.13.0
-3
View File
@@ -31,7 +31,4 @@ module.exports = {
isDevContainer() {
return isTrue(process.env.DEV_CONTAINER);
},
jsMinifier() {
return (process.env.JS_MINIFIER || "swc").toLowerCase();
},
};
+1 -7
View File
@@ -80,13 +80,7 @@ const doneHandler = (done) => (err, stats) => {
console.log(stats.toString("minimal"));
}
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}`);
log(`Build done @ ${new Date().toLocaleTimeString()}`);
if (done) {
done();
+5 -15
View File
@@ -13,7 +13,6 @@ 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");
@@ -101,20 +100,11 @@ const createRspackConfig = ({
},
optimization: {
minimizer: [
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 },
},
}),
new TerserPlugin({
parallel: true,
extractComments: true,
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
}),
],
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
+1 -1
View File
@@ -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. Theres a lot a designer can contribute to:
- 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.
- 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.
- 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!
+7 -7
View File
@@ -29,7 +29,7 @@
"@babel/runtime": "7.28.6",
"@braintree/sanitize-url": "7.1.2",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/commands": "6.10.2",
"@codemirror/commands": "6.10.1",
"@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.2.1-ha.0",
"@home-assistant/webawesome": "3.0.0-ha.2",
"@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.9",
"ua-parser-js": "2.0.8",
"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.2",
"@rspack/core": "1.7.5",
"@rsdoctor/rspack-plugin": "1.5.1",
"@rspack/core": "1.7.4",
"@rspack/dev-server": "1.2.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.25",
@@ -191,7 +191,7 @@
"eslint-plugin-wc": "3.0.2",
"fancy-log": "2.0.0",
"fs-extra": "11.3.3",
"glob": "13.0.1",
"glob": "13.0.0",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0",
@@ -235,6 +235,6 @@
},
"packageManager": "yarn@4.12.0",
"volta": {
"node": "24.13.1"
"node": "24.13.0"
}
}
-43
View File
@@ -1,43 +0,0 @@
#!/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"
-3
View File
@@ -116,6 +116,3 @@ 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 = " · ";
+44 -159
View File
@@ -3,14 +3,13 @@ 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, ValuePart } from "../../types";
import type { HomeAssistant } 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";
@@ -52,36 +51,8 @@ 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 [
{
type: "value",
value: localize(`state.default.${state}`),
},
];
return localize(`state.default.${state}`);
}
const domain = computeDomain(entityId);
@@ -102,27 +73,19 @@ const computeStateToPartsFromEntityAttributes = (
DURATION_UNITS.includes(attributes.unit_of_measurement)
) {
try {
return [
{
type: "value",
value: formatDuration(
locale,
state,
attributes.unit_of_measurement,
entity?.display_precision
),
},
];
return 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 {
parts = formatNumberToParts(state, locale, {
return formatNumber(state, locale, {
style: "currency",
currency: attributes.unit_of_measurement,
minimumFractionDigits: 2,
@@ -135,34 +98,8 @@ const computeStateToPartsFromEntityAttributes = (
} 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,
@@ -177,14 +114,10 @@ const computeStateToPartsFromEntityAttributes = (
attributes.unit_of_measurement;
if (unit) {
return [
{ type: "value", value: value },
{ type: "literal", value: blankBeforeUnit(unit, locale) },
{ type: "unit", value: unit },
];
return `${value}${blankBeforeUnit(unit, locale)}${unit}`;
}
return [{ type: "value", value: value }];
return value;
}
if (["date", "input_datetime", "time"].includes(domain)) {
@@ -196,51 +129,36 @@ const computeStateToPartsFromEntityAttributes = (
const components = state.split(" ");
if (components.length === 2) {
// Date and time.
return [
{
type: "value",
value: formatDateTime(
new Date(components.join("T")),
{ ...locale, time_zone: TimeZone.local },
config
),
},
];
return formatDateTime(
new Date(components.join("T")),
{ ...locale, time_zone: TimeZone.local },
config
);
}
if (components.length === 1) {
if (state.includes("-")) {
// Date only.
return [
{
type: "value",
value: formatDate(
new Date(`${state}T00:00`),
{ ...locale, time_zone: TimeZone.local },
config
),
},
];
return formatDate(
new Date(`${state}T00:00`),
{ ...locale, time_zone: TimeZone.local },
config
);
}
if (state.includes(":")) {
// Time only.
const now = new Date();
return [
{
type: "value",
value: formatTime(
new Date(`${now.toISOString().split("T")[0]}T${state}`),
{ ...locale, time_zone: TimeZone.local },
config
),
},
];
return formatTime(
new Date(`${now.toISOString().split("T")[0]}T${state}`),
{ ...locale, time_zone: TimeZone.local },
config
);
}
}
return [{ type: "value", value: state }];
return state;
} catch (_e) {
// Formatting methods may throw error if date parsing doesn't go well,
// just return the state string in that case.
return [{ type: "value", value: state }];
return state;
}
}
@@ -264,58 +182,25 @@ const computeStateToPartsFromEntityAttributes = (
(domain === "sensor" && attributes.device_class === "timestamp")
) {
try {
return [
{
type: "value",
value: formatDateTime(new Date(state), locale, config),
},
];
return formatDateTime(new Date(state), locale, config);
} catch (_err) {
return [{ type: "value", value: state }];
return 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
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
);
};
+10 -19
View File
@@ -5,6 +5,7 @@ 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
@@ -51,22 +52,7 @@ export const formatNumber = (
num: string | number,
localeOptions?: FrontendLocaleData,
options?: Intl.NumberFormatOptions
): 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[] => {
): string => {
const locale = localeOptions
? numberFormatToLocale(localeOptions)
: undefined;
@@ -85,7 +71,7 @@ export const formatNumberToParts = (
return new Intl.NumberFormat(
locale,
getDefaultFormatOptions(num, options)
).formatToParts(Number(num));
).format(Number(num));
}
if (
@@ -100,10 +86,15 @@ export const formatNumberToParts = (
...options,
useGrouping: false,
})
).formatToParts(Number(num));
).format(Number(num));
}
return [{ type: "literal", value: num }];
if (typeof num === "string") {
return num;
}
return `${round(num, options?.maximumFractionDigits).toString()}${
options?.style === "currency" ? ` ${options.currency}` : ""
}`;
};
/**
@@ -1,28 +0,0 @@
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;
};
+1 -16
View File
@@ -12,10 +12,6 @@ export type FormatEntityStateFunc = (
stateObj: HassEntity,
state?: string
) => string;
export type FormatEntityStateToPartsFunc = (
stateObj: HassEntity,
state?: string
) => ValuePart[];
export type FormatEntityAttributeValueFunc = (
stateObj: HassEntity,
attribute: string,
@@ -50,13 +46,12 @@ export const computeFormatFunctions = async (
sensorNumericDeviceClasses: string[]
): Promise<{
formatEntityState: FormatEntityStateFunc;
formatEntityStateToParts: FormatEntityStateToPartsFunc;
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
formatEntityAttributeValueToParts: FormatEntityAttributeValueToPartsFunc;
formatEntityAttributeName: FormatEntityAttributeNameFunc;
formatEntityName: FormatEntityNameFunc;
}> => {
const { computeStateDisplay, computeStateToParts } =
const { computeStateDisplay } =
await import("../entity/compute_state_display");
const {
computeAttributeValueDisplay,
@@ -75,16 +70,6 @@ export const computeFormatFunctions = async (
entities,
state
),
formatEntityStateToParts: (stateObj, state) =>
computeStateToParts(
localize,
stateObj,
locale,
sensorNumericDeviceClasses,
config,
entities,
state
),
formatEntityAttributeValue: (stateObj, attribute, value) =>
computeAttributeValueDisplay(
localize,
+11 -17
View File
@@ -572,7 +572,6 @@ 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;
}
@@ -602,25 +601,10 @@ export class StatisticsChart extends LitElement {
dataValues.push(val);
});
if (!this._hiddenStats.has(statistic_id)) {
pushData(
startDate,
endDate.getTime() < endTime.getTime() ? endDate : endTime,
dataValues
);
pushData(startDate, new Date(stat.end), 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
@@ -635,6 +619,16 @@ 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)[] = [];
+1 -3
View File
@@ -20,7 +20,6 @@ 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";
@@ -637,7 +636,7 @@ export class HaDataTable extends LitElement {
.map(
([key2, column2], i) =>
html`${i !== 0
? STRINGS_SEPARATOR_DOT
? " · "
: nothing}${column2.template
? column2.template(row)
: row[key2]}`
@@ -1193,7 +1192,6 @@ export class HaDataTable extends LitElement {
.mdc-data-table__cell--numeric {
text-align: var(--float-end);
direction: ltr;
}
.mdc-data-table__cell--icon {
+20 -10
View File
@@ -9,7 +9,16 @@ 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 { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
import {
formatNumber,
getNumberFormatOptions,
isNumericState,
} from "../../common/number/format_number";
import {
isUnavailableState,
UNAVAILABLE,
UNKNOWN,
} from "../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import { timerTimeRemaining } from "../../data/timer";
import type { HomeAssistant } from "../../types";
@@ -171,11 +180,16 @@ export class HaStateLabelBadge extends LitElement {
}
// eslint-disable-next-line: disable=no-fallthrough
default:
return isUnavailableState(entityState.state)
return entityState.state === UNKNOWN ||
entityState.state === UNAVAILABLE
? "—"
: this.hass!.formatEntityStateToParts(entityState).find(
(part) => part.type === "value"
)?.value;
: isNumericState(entityState)
? formatNumber(
entityState.state,
this.hass!.locale,
getNumberFormatOptions(entityState, entry)
)
: this.hass!.formatEntityState(entityState);
}
}
@@ -224,11 +238,7 @@ export class HaStateLabelBadge extends LitElement {
if (domain === "timer") {
return secondsToDuration(_timerTimeRemaining);
}
return (
this.hass!.formatEntityStateToParts(entityState).find(
(part) => part.type === "unit"
)?.value || null
);
return entityState.attributes.unit_of_measurement || null;
}
private _clearInterval() {
+1 -1
View File
@@ -163,7 +163,7 @@ export class HaAreaPicker extends LitElement {
{
id: ADD_NEW_ID + searchString,
primary: this.hass.localize(
"ui.components.area-picker.add_new_suggestion",
"ui.components.area-picker.add_new_sugestion",
{
name: searchString,
}
+2 -2
View File
@@ -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, ValueChangedEvent } from "../types";
import type { HomeAssistant } 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: ValueChangedEvent<string | undefined>) {
private _valueChanged(ev: CustomEvent<{ value?: string }>) {
ev.stopPropagation();
const selected = ev.detail.value;
const normalized =
+10 -1
View File
@@ -89,7 +89,7 @@ export class HaControlSelectMenu extends LitElement {
private _renderOption = (option: SelectOption) =>
html`<ha-dropdown-item
.value=${option.value}
.selected=${this.value === option.value}
class=${this.value === option.value ? "selected" : ""}
>${option.iconPath
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
: option.icon
@@ -263,6 +263,15 @@ 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);
+38 -61
View File
@@ -7,9 +7,8 @@ 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 {
@@ -23,8 +22,6 @@ export class HaDialogDatePicker extends LitElement {
@state() private _params?: DatePickerDialogParams;
@state() private _open = false;
@state() private _value?: string;
public async showDialog(params: DatePickerDialogParams): Promise<void> {
@@ -33,14 +30,9 @@ 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 });
}
@@ -49,13 +41,7 @@ export class HaDialogDatePicker extends LitElement {
if (!this._params) {
return nothing;
}
return html`<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
width="small"
without-header
@closed=${this._dialogClosed}
>
return html`<ha-dialog open @closed=${this.closeDialog}>
<app-datepicker
.value=${this._value}
.min=${this._params.min}
@@ -64,40 +50,35 @@ export class HaDialogDatePicker extends LitElement {
@datepicker-value-updated=${this._valueChanged}
.firstDayOfWeek=${this._params.firstWeekday}
></app-datepicker>
<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>`;
${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>`;
}
private _valueChanged(ev: CustomEvent) {
@@ -127,20 +108,11 @@ export class HaDialogDatePicker extends LitElement {
static styles = [
haStyleDialog,
css`
ha-wa-dialog {
ha-dialog {
--dialog-content-padding: 0;
}
.bottom-actions {
display: flex;
gap: var(--ha-space-4);
justify-content: center;
align-items: center;
width: 100%;
margin-bottom: var(--ha-space-1);
--justify-action-buttons: space-between;
}
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);
@@ -157,6 +129,11 @@ 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%;
+1 -10
View File
@@ -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, property } from "lit/decorators";
import { customElement } from "lit/decorators";
import "./ha-svg-icon";
/**
@@ -17,8 +17,6 @@ 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
@@ -49,13 +47,6 @@ 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);
}
`,
];
}
-27
View File
@@ -1,4 +1,3 @@
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";
@@ -23,37 +22,11 @@ export type HaDropdownSelectEvent<T = string> = CustomEvent<{
*
*/
@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,
+1 -4
View File
@@ -6,7 +6,6 @@ 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;
@@ -153,9 +152,7 @@ class HaDurationInput extends LitElement {
: NaN;
}
private _durationChanged(
ev: ValueChangedEvent<TimeChangedEvent | undefined>
) {
private _durationChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
ev.stopPropagation();
const value = ev.detail.value ? { ...ev.detail.value } : undefined;
+1 -5
View File
@@ -315,13 +315,9 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
}
ha-list {
--mdc-list-item-meta-size: auto;
--mdc-list-side-padding-right: var(--ha-space-1);
--mdc-list-side-padding-left: var(--ha-space-4);
--mdc-list-side-padding-right: 4px;
--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);
}
-3
View File
@@ -179,9 +179,6 @@ 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;
-3
View File
@@ -199,9 +199,6 @@ 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,9 +164,6 @@ 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;
+1 -1
View File
@@ -359,7 +359,7 @@ export class HaFloorPicker extends LitElement {
{
id: ADD_NEW_ID + searchString,
primary: this.hass.localize(
"ui.components.floor-picker.add_new_suggestion",
"ui.components.floor-picker.add_new_sugestion",
{
name: searchString,
}
+1
View File
@@ -9,6 +9,7 @@ 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";
+1 -1
View File
@@ -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_suggestion",
"ui.components.label-picker.add_new_sugestion",
{
name: searchString,
}
+22
View File
@@ -0,0 +1,22 @@
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;
}
}
+52
View File
@@ -0,0 +1,52 @@
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;
}
}
+47
View File
@@ -0,0 +1,47 @@
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;
}
}
+6 -1
View File
@@ -135,7 +135,9 @@ class HaQrScanner extends LitElement {
(camera) => html`
<ha-dropdown-item
.value=${camera.id}
.selected=${this._selectedCamera === camera.id}
class=${this._selectedCamera === camera.id
? "selected"
: ""}
>
${camera.label}
</ha-dropdown-item>
@@ -378,6 +380,9 @@ 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;
+16 -2
View File
@@ -94,8 +94,10 @@ export class HaSelect extends LitElement {
.disabled=${typeof option === "string"
? false
: (option.disabled ?? false)}
.selected=${this.value ===
(typeof option === "string" ? option : option.value)}
class=${this.value ===
(typeof option === "string" ? option : option.value)
? "selected"
: ""}
>
${option.iconPath
? html`<ha-svg-icon
@@ -180,6 +182,10 @@ 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);
@@ -194,6 +200,14 @@ 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 {
+1 -2
View File
@@ -5,7 +5,6 @@ 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 {
@@ -70,7 +69,7 @@ export class HaTimeInput extends LitElement {
`;
}
private _timeChanged(ev: ValueChangedEvent<TimeChangedEvent | undefined>) {
private _timeChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
ev.stopPropagation();
const eventValue = ev.detail.value;
+16 -16
View File
@@ -14,7 +14,6 @@ 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";
@@ -198,21 +197,22 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
await this.updateComplete;
requestAnimationFrame(() => {
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;
}
// 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;
// }
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
});
};
@@ -24,8 +24,6 @@ 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",
@@ -168,9 +166,7 @@ export class HaTracePathDetails extends LitElement {
: selectedType === "trigger"
? html`<h2>
${describeTrigger(
migrateAutomationTrigger({
...currentDetail,
}) as Trigger,
currentDetail,
this.hass,
this._entityReg
)}
+3 -4
View File
@@ -32,13 +32,12 @@ 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;
}
`,
];
+20 -5
View File
@@ -14,7 +14,6 @@ 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,
@@ -1432,10 +1431,26 @@ export const getPowerFromState = (stateObj: HassEntity): number | undefined => {
return undefined;
}
return normalizeValueBySIPrefix(
value,
stateObj.attributes.unit_of_measurement
);
// 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;
}
};
/**
+1 -2
View File
@@ -142,7 +142,7 @@ export const subscribeHistory = (
);
};
export class HistoryStream {
class HistoryStream {
hass: HomeAssistant;
hoursToShow?: number;
@@ -221,7 +221,6 @@ export 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);
}
}
+2 -6
View File
@@ -41,16 +41,12 @@ export const enum TodoListEntityFeature {
SET_DESCRIPTION_ON_ITEM = 64,
}
export const getTodoLists = (
hass: HomeAssistant,
includeHidden = true
): TodoList[] =>
export const getTodoLists = (hass: HomeAssistant): TodoList[] =>
Object.keys(hass.states)
.filter(
(entityId) =>
computeDomain(entityId) === "todo" &&
!isUnavailableState(hass.states[entityId].state) &&
(includeHidden || hass.entities[entityId]?.hidden !== true)
!isUnavailableState(hass.states[entityId].state)
)
.map((entityId) => ({
...hass.states[entityId],
@@ -213,7 +213,9 @@ class MoreInfoMediaPlayer extends LitElement {
(source) =>
html`<ha-dropdown-item
.value=${source}
.selected=${source === this.stateObj?.attributes.source}
class=${source === this.stateObj?.attributes.source
? "selected"
: ""}
>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
@@ -248,7 +250,9 @@ class MoreInfoMediaPlayer extends LitElement {
(soundMode) =>
html`<ha-dropdown-item
.value=${soundMode}
.selected=${soundMode === this.stateObj?.attributes.sound_mode}
class=${soundMode === this.stateObj?.attributes.sound_mode
? "selected"
: ""}
>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
@@ -674,6 +678,13 @@ 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 {
@@ -313,119 +313,113 @@ class MoreInfoWeather extends LitElement {
</div>
`
: nothing}
${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}
<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.map(
(forecastType) =>
html`<ha-tab-group-tab
slot="nav"
.panel=${forecastType}
.active=${this._forecastType === forecastType}
>
${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
)}
</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>
${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.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
)}
</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>
${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.stateObj.attributes.attribution
? html`
<div class="attribution">
@@ -129,12 +129,11 @@ 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}
.selected=${this._language === lang.id}
class=${this._language === lang.id ? "selected" : ""}
>
${lang.primary}
</ha-dropdown-item>`
@@ -407,6 +407,13 @@ 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);
}
`,
];
}
-9
View File
@@ -52,7 +52,6 @@ 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,
@@ -118,7 +117,6 @@ export const provideHass = (
async function updateFormatFunctions() {
const {
formatEntityState,
formatEntityStateToParts,
formatEntityAttributeName,
formatEntityAttributeValue,
formatEntityAttributeValueToParts,
@@ -135,7 +133,6 @@ export const provideHass = (
);
hass().updateHass({
formatEntityState,
formatEntityStateToParts,
formatEntityAttributeName,
formatEntityAttributeValue,
formatEntityAttributeValueToParts,
@@ -378,12 +375,6 @@ 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] ?? ""),
+1 -3
View File
@@ -74,8 +74,6 @@ 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[] = [];
@@ -210,7 +208,7 @@ export class HAFullCalendar extends LitElement {
: ""}
<div id="calendar"></div>
${this.addFab && this._hasMutableCalendars
${this._hasMutableCalendars
? html`<ha-fab
slot="fab"
.label=${this.hass.localize("ui.components.calendar.event.add")}
-5
View File
@@ -193,7 +193,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
</ha-list-item>`
: nothing}
<ha-full-calendar
add-fab
.events=${this._events}
.calendars=${this._calendars}
.narrow=${this.narrow}
@@ -331,8 +330,6 @@ 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);
}
@@ -342,8 +339,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
:host([mobile]) {
padding-left: unset;
padding-inline-start: unset;
padding-inline-end: initial;
}
.loading {
display: flex;
@@ -63,7 +63,6 @@ class SupervisorAppDocumentationDashboard extends LitElement {
margin: auto;
padding: var(--ha-space-2);
max-width: 1024px;
direction: ltr;
}
ha-markdown {
padding: var(--ha-space-4);
@@ -102,7 +102,7 @@ export class HaConditionAction
}
return html`
<ha-dropdown-item .value=${opt} .selected=${selected}>
<ha-dropdown-item .value=${opt} class=${selected ? "selected" : ""}>
<ha-condition-icon
.hass=${this.hass}
slot="icon"
@@ -1,4 +1,3 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
@@ -43,6 +42,7 @@ 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";
@@ -657,7 +657,10 @@ class DialogAddAutomationElement
.path=${mdiPlus}
></ha-svg-icon>
</ha-md-list-item>
<wa-divider></wa-divider>`
<ha-md-divider
role="separator"
tabindex="-1"
></ha-md-divider>`
: nothing}
${collections.map(
(collection, index) => html`
@@ -2174,8 +2177,8 @@ class DialogAddAutomationElement
width: var(--ha-space-6);
}
wa-divider {
--spacing: 0;
ha-md-list-item.paste {
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
}
ha-svg-icon.plus {
@@ -47,10 +47,6 @@ 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";
@@ -61,6 +57,10 @@ 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 { 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 {
getEntityIdHiddenTableColumn,
getAreaTableColumn,
getCategoryTableColumn,
getEntityIdHiddenTableColumn,
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 { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import {
@@ -129,6 +129,7 @@ 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;
@@ -222,7 +223,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
})
private _activeHiddenColumns?: string[];
@query("#overflow-menu") private _overflowMenu!: HaDropdown;
@query("#overflow-menu") private _overflowMenu!: HaMdMenu;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
@@ -232,8 +233,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
return getAvailableAssistants(this.cloudStatus, this.hass);
}
private _openingOverflow = false;
private _automations = memoizeOne(
(
automations: AutomationEntity[],
@@ -372,27 +371,16 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
);
private _showOverflowMenu = (ev) => {
if (this._overflowMenu.anchorElement === ev.target) {
this._overflowMenu.anchorElement = undefined;
if (
this._overflowMenu.open &&
ev.target === this._overflowMenu.anchorElement
) {
this._overflowMenu.close();
return;
}
this._openingOverflow = true;
this._overflowMenu.anchorElement = ev.target;
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;
this._overflowMenu.anchorElement = ev.target;
this._overflowMenu.show();
};
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
@@ -709,58 +697,74 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
<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 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-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-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-svg-icon
.path=${this._overflowAutomation?.state === "off"
? mdiToggleSwitch
: mdiToggleSwitchOffOutline}
slot="icon"
slot="start"
></ha-svg-icon>
${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>
<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>
`;
}
@@ -897,59 +901,33 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this._applyFilters();
}
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 _showInfo = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
fireEvent(this, "hass-more-info", { entityId: automation.entity_id });
};
private _showInfo = (automation: AutomationItem) => {
fireEvent(this, "hass-more-info", {
entityId: automation.entity_id,
});
};
private _showSettings = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _showSettings = (automation: AutomationItem) => {
fireEvent(this, "hass-more-info", {
entityId: automation.entity_id,
view: "settings",
});
};
private _runActions = (automation: AutomationItem) => {
private _runActions = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
triggerAutomationActions(this.hass, automation.entity_id);
};
private _editCategory = (automation: AutomationItem) => {
private _editCategory = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
const entityReg = this._entityReg.find(
(reg) => reg.entity_id === automation.entity_id
);
@@ -970,7 +948,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
});
};
private _showTrace = (automation: AutomationItem) => {
private _showTrace = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
if (!automation.attributes.id) {
showAlertDialog(this, {
text: this.hass.localize(
@@ -984,14 +965,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
);
};
private _toggle = async (automation: AutomationItem): Promise<void> => {
private _toggle = async (item: HaMdMenuItem): Promise<void> => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
const service = automation.state === "off" ? "turn_on" : "turn_off";
await this.hass.callService("automation", service, {
entity_id: automation.entity_id,
});
};
private _deleteConfirm = async (automation: AutomationItem) => {
private _deleteConfirm = async (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.picker.delete_confirm_title"
@@ -1007,9 +994,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
});
};
private async _delete(automation: AutomationItem) {
private async _delete(automation) {
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
);
@@ -1028,11 +1015,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}
}
private _duplicate = async (automation: AutomationItem) => {
private _duplicate = async (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
try {
const config = await fetchAutomationFileConfig(
this.hass,
automation.attributes.id!
automation.attributes.id
);
duplicateAutomation(config);
} catch (err: any) {
@@ -438,6 +438,7 @@ export class HaManualAutomationEditor extends SubscribeMixin(LitElement) {
}
private _saveAutomation() {
this.triggerCloseSidebar();
fireEvent(this, "save-automation");
}
+6
View File
@@ -40,6 +40,9 @@ export const rowStyles = css`
.warning ul {
margin: 4px 0;
}
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
ha-tooltip {
cursor: default;
}
@@ -269,4 +272,7 @@ export const overflowStyles = css`
display: none;
}
}
ha-md-menu-item {
--mdc-icon-size: 24px;
}
`;
@@ -4,8 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-wa-dialog";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
@@ -37,20 +36,13 @@ 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;
@@ -63,13 +55,17 @@ class LocalBackupLocationDialog extends LitElement {
return nothing;
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.title`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.title`
)
)}
@closed=${this._dialogClosed}
@closed=${this.closeDialog}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
@@ -81,35 +77,34 @@ 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-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>
<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>
`;
}
@@ -148,6 +143,9 @@ class LocalBackupLocationDialog extends LitElement {
haStyle,
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
ha-form {
display: block;
margin-bottom: 16px;
@@ -26,16 +26,15 @@ import type {
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-button";
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-states";
import "../../../components/ha-icon";
import "../../../components/ha-icon-next";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-menu";
import type { HaMdMenu } from "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import type {
@@ -74,6 +73,7 @@ import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
import { downloadBackup } from "./helper/download_backup";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
interface BackupRow extends DataTableRowData, BackupContent {
formatted_type: string;
@@ -123,11 +123,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
@query("#overflow-menu") private _overflowMenu?: HaDropdown;
private _openingOverflow = false;
private _overflowBackup?: BackupRow;
@query("#overflow-menu") private _overflowMenu?: HaMdMenu;
public connectedCallback() {
super.connectedCallback();
@@ -291,27 +287,12 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
return;
}
if (this._overflowMenu.anchorElement === ev.target) {
this._overflowMenu.anchorElement = undefined;
if (this._overflowMenu.open) {
this._overflowMenu.close();
return;
}
this._openingOverflow = true;
this._overflowMenu.anchorElement = ev.target;
this._overflowBackup = ev.target.backup;
this._overflowMenu.open = true;
};
private _overflowMenuOpened = () => {
this._openingOverflow = false;
};
private _overflowMenuClosed = () => {
// changing the anchorElement triggers a close event, ignore it
if (this._openingOverflow || !this._overflowMenu) {
return;
}
this._overflowMenu.anchorElement = undefined;
this._overflowMenu.show();
};
private _handleGroupingChanged(ev: CustomEvent) {
@@ -496,21 +477,16 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
`
: nothing}
</hass-tabs-subpage-data-table>
<ha-dropdown
id="overflow-menu"
@wa-select=${this._handleOverflowAction}
@wa-after-show=${this._overflowMenuOpened}
@wa-after-hide=${this._overflowMenuClosed}
>
<ha-dropdown-item value="download">
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
<ha-md-menu id="overflow-menu" positioning="fixed">
<ha-md-menu-item .clickAction=${this._downloadBackup}>
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
${this.hass.localize("ui.common.download")}
</ha-dropdown-item>
<ha-dropdown-item variant="danger" value="delete">
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item class="warning" .clickAction=${this._deleteBackup}>
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
${this.hass.localize("ui.common.delete")}
</ha-dropdown-item>
</ha-dropdown>
</ha-md-menu-item>
</ha-md-menu>
`;
}
@@ -580,29 +556,16 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
navigate(`/config/backup/details/${id}`);
}
private _handleOverflowAction = (ev: HaDropdownSelectEvent) => {
const action = ev.detail.item.value;
if (action === "download") {
this._downloadBackup();
return;
}
if (action === "delete") {
this._deleteBackup();
}
};
private _downloadBackup = async (): Promise<void> => {
const backup = this._overflowBackup;
private _downloadBackup = async (ev): Promise<void> => {
const backup = ev.parentElement.anchorElement.backup;
if (!backup) {
return;
}
downloadBackup(this.hass, this, backup, this.config);
};
private _deleteBackup = async (): Promise<void> => {
const backup = this._overflowBackup;
private _deleteBackup = async (ev): Promise<void> => {
const backup = ev.parentElement.anchorElement.backup;
if (!backup) {
return;
}
@@ -6,19 +6,17 @@ import { documentationUrl } from "../../../util/documentation-url";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-code-editor";
import "../../../components/ha-dialog";
import "../../../components/ha-dialog-header";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-markdown";
import "../../../components/ha-spinner";
import "../../../components/ha-textfield";
import "../../../components/ha-wa-dialog";
import type { HaTextField } from "../../../components/ha-textfield";
import type { BlueprintImportResult } from "../../../data/blueprint";
import { importBlueprint, saveBlueprint } from "../../../data/blueprint";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { withViewTransition } from "../../../common/util/view-transition";
@customElement("ha-dialog-import-blueprint")
class DialogImportBlueprint extends LitElement {
@@ -28,8 +26,6 @@ class DialogImportBlueprint extends LitElement {
@state() private _params?;
@state() private _open = false;
@state() private _importing = false;
@state() private _saving = false;
@@ -47,14 +43,9 @@ class DialogImportBlueprint extends LitElement {
this._error = undefined;
this._url = this._params.url;
this.large = false;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._error = undefined;
this._result = undefined;
this._params = undefined;
@@ -68,16 +59,11 @@ class DialogImportBlueprint extends LitElement {
}
const heading = this.hass.localize("ui.panel.config.blueprint.add.header");
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
width=${this.large ? "full" : "medium"}
@closed=${this._dialogClosed}
>
<ha-dialog-header slot="header">
<ha-dialog open .heading=${heading} @closed=${this.closeDialog}>
<ha-dialog-header slot="heading">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
dialogAction="cancel"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
@@ -118,7 +104,6 @@ class DialogImportBlueprint extends LitElement {
.label=${this.hass.localize(
"ui.panel.config.blueprint.add.file_name"
)}
autofocus
></ha-textfield>
`}
<ha-expansion-panel
@@ -172,63 +157,59 @@ class DialogImportBlueprint extends LitElement {
"ui.panel.config.blueprint.add.url"
)}
.value=${this._url || ""}
autofocus
dialogInitialFocus
></ha-textfield>
`}
</div>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._saving}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
${!this._result
? html`
<ha-button
slot="primaryAction"
@click=${this._import}
.disabled=${this._importing}
.loading=${this._importing}
.ariaLabel=${this.hass.localize(
`ui.panel.config.blueprint.add.${this._importing ? "importing" : "import_btn"}`
)}
>
${this.hass.localize(
"ui.panel.config.blueprint.add.import_btn"
)}
</ha-button>
`
: html`
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${this._saving || !!this._result.validation_errors}
.loading=${this._saving}
.ariaLabel=${this.hass.localize(
`ui.panel.config.blueprint.add.${this._saving ? "saving" : this._result.exists ? "save_btn_override" : "save_btn"}`
)}
>
${this._result.exists
? this.hass.localize(
"ui.panel.config.blueprint.add.save_btn_override"
)
: this.hass.localize(
"ui.panel.config.blueprint.add.save_btn"
)}
</ha-button>
`}
</ha-dialog-footer>
</ha-wa-dialog>
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._saving}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
${!this._result
? html`
<ha-button
slot="primaryAction"
@click=${this._import}
.disabled=${this._importing}
.loading=${this._importing}
.ariaLabel=${this.hass.localize(
`ui.panel.config.blueprint.add.${this._importing ? "importing" : "import_btn"}`
)}
>
${this.hass.localize(
"ui.panel.config.blueprint.add.import_btn"
)}
</ha-button>
`
: html`
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${this._saving || !!this._result.validation_errors}
.loading=${this._saving}
.ariaLabel=${this.hass.localize(
`ui.panel.config.blueprint.add.${this._saving ? "saving" : this._result.exists ? "save_btn_override" : "save_btn"}`
)}
>
${this._result.exists
? this.hass.localize(
"ui.panel.config.blueprint.add.save_btn_override"
)
: this.hass.localize(
"ui.panel.config.blueprint.add.save_btn"
)}
</ha-button>
`}
</ha-dialog>
`;
}
private _enlarge() {
withViewTransition(() => {
this.large = !this.large;
});
this.large = !this.large;
}
private async _import() {
@@ -292,6 +273,10 @@ class DialogImportBlueprint extends LitElement {
a ha-svg-icon {
--mdc-icon-size: 16px;
}
:host([large]) ha-dialog {
--mdc-dialog-min-width: 90vw;
--mdc-dialog-max-width: 90vw;
}
ha-expansion-panel {
--expansion-panel-content-padding: 0px;
}
@@ -153,7 +153,7 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
{
id: ADD_NEW_ID + searchString,
primary: this.hass.localize(
"ui.components.category-picker.add_new_suggestion",
"ui.components.category-picker.add_new_sugestion",
{
name: searchString,
}
@@ -1,49 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../layouts/hass-subpage";
import "./ai-task-pref";
import type { HomeAssistant } from "../../../types";
@customElement("ha-config-section-ai-tasks")
class HaConfigSectionAITasks extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
protected render() {
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.ai_tasks.caption")}
>
<div class="content">
<ai-task-pref
.hass=${this.hass}
.narrow=${this.narrow}
></ai-task-pref>
</div>
</hass-subpage>
`;
}
static styles = css`
.content {
padding: var(--ha-space-7) var(--ha-space-5) 0;
max-width: 1040px;
margin: 0 auto;
}
ai-task-pref {
max-width: 600px;
margin: 0 auto;
display: block;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-section-ai-tasks": HaConfigSectionAITasks;
}
}
@@ -71,7 +71,6 @@ class HaConfigSectionAnalytics extends LitElement {
display: block;
max-width: 600px;
margin: 0 auto;
margin-bottom: 24px;
}
`;
}
@@ -1,6 +1,6 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, state, query } from "lit/decorators";
import { UNIT_C } from "../../../common/const";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { navigate } from "../../../common/navigate";
@@ -17,6 +17,8 @@ import "../../../components/ha-formfield";
import "../../../components/ha-language-picker";
import "../../../components/ha-radio";
import type { HaRadio } from "../../../components/ha-radio";
import "../../../components/ha-select";
import "../../../components/ha-settings-row";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import "../../../components/ha-timezone-picker";
@@ -24,7 +26,8 @@ import type { ConfigUpdateValues } from "../../../data/core";
import { saveCoreConfig } from "../../../data/core";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage";
import "../../../components/map/ha-map";
import "./ai-task-pref";
import type { AITaskPref } from "./ai-task-pref";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@@ -34,9 +37,7 @@ class HaConfigSectionGeneral extends LitElement {
@property({ type: Boolean }) public narrow = false;
@state() private _submittingName = false;
@state() private _submittingRegional = false;
@state() private _submitting = false;
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
@@ -58,10 +59,13 @@ class HaConfigSectionGeneral extends LitElement {
@state() private _updateUnits?: boolean;
@query("ai-task-pref") private _aiTaskPref!: AITaskPref;
protected render(): TemplateResult {
const canEdit = ["storage", "default"].includes(
this.hass.config.config_source
);
const disabled = this._submitting || !canEdit;
return html`
<hass-subpage
back-path="/config/system"
@@ -73,269 +77,211 @@ class HaConfigSectionGeneral extends LitElement {
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${!canEdit
? html`
<ha-alert>
<ha-card outlined>
<div class="card-content">
${!canEdit
? html`
<ha-alert>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
)}
</ha-alert>
`
: nothing}
<ha-textfield
name="name"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.location_name"
)}
.disabled=${disabled}
.value=${this._name}
@change=${this._handleChange}
></ha-textfield>
<ha-timezone-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.time_zone"
)}
name="timeZone"
.disabled=${disabled}
.value=${this._timeZone}
@value-changed=${this._handleValueChanged}
hide-clear-icon
></ha-timezone-picker>
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation"
)}
name="elevation"
type="number"
.disabled=${disabled}
.value=${this._elevation}
.suffix=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)}
@change=${this._handleChange}
>
</ha-textfield>
<div>
<div>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
"ui.panel.config.core.section.core.core_config.unit_system"
)}
</ha-alert>
`
: nothing}
${this._renderHomeNameCard(canEdit)}
${this._renderLocationCard(canEdit)}
${this._renderRegionalSettingsCard(canEdit)}
</div>
<ha-formfield
.label=${html`
<span style="font-size: 14px">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.metric_example"
)}
</span>
<div style="color: var(--secondary-text-color)">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_metric"
)}
</div>
`}
>
<ha-radio
name="unit_system"
value="metric"
.checked=${this._unitSystem === "metric"}
@change=${this._unitSystemChanged}
.disabled=${disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`
<span style="font-size: 14px">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.us_customary_example"
)}
</span>
<div style="color: var(--secondary-text-color)">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_us_customary"
)}
</div>
`}
>
<ha-radio
name="unit_system"
value="us_customary"
.checked=${this._unitSystem === "us_customary"}
@change=${this._unitSystemChanged}
.disabled=${disabled}
></ha-radio>
</ha-formfield>
${this._unitSystem !== this._configuredUnitSystem()
? html`
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_label"
)}
>
<ha-checkbox
.checked=${this._updateUnits}
.disabled=${this._submitting}
@change=${this._updateUnitsChanged}
></ha-checkbox>
</ha-formfield>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_text_1"
)}
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_text_2"
)} <br /><br />
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_text_3"
)}
</div>
`
: ""}
</div>
<div>
<ha-currency-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.currency"
)}
name="currency"
.disabled=${disabled}
.value=${this._currency}
@value-changed=${this._handleValueChanged}
></ha-currency-picker>
<a
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
target="_blank"
rel="noopener noreferrer"
class="find-value"
>${this.hass.localize(
"ui.panel.config.core.section.core.core_config.find_currency_value"
)}</a
>
</div>
<ha-country-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.country"
)}
name="country"
.disabled=${disabled}
.value=${this._country}
@value-changed=${this._handleValueChanged}
></ha-country-picker>
<ha-language-picker
.hass=${this.hass}
native-name
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.language"
)}
name="language"
.value=${this._language}
.disabled=${disabled}
@closed=${stopPropagation}
@value-changed=${this._handleValueChanged}
>
</ha-language-picker>
</div>
<ha-settings-row>
<div slot="heading">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_location"
)}
</div>
<div slot="description" class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_location_description"
)}
</div>
<ha-button
appearance="plain"
size="small"
@click=${this._editLocation}
.disabled=${disabled}
>${this.hass.localize("ui.common.edit")}</ha-button
>
</ha-settings-row>
<div class="card-actions">
<ha-progress-button
@click=${this._updateEntry}
.disabled=${disabled}
>
${this.hass!.localize("ui.common.save")}
</ha-progress-button>
</div>
</ha-card>
<ai-task-pref
.hass=${this.hass}
.narrow=${this.narrow}
></ai-task-pref>
</div>
</hass-subpage>
`;
}
private _renderHomeNameCard(canEdit: boolean): TemplateResult {
const disabled = this._submittingName || !canEdit;
return html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.core.section.core.home_name_card.header"
)}
>
<div class="card-content">
<ha-textfield
name="name"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.location_name"
)}
.disabled=${disabled}
.value=${this._name}
@change=${this._handleChange}
></ha-textfield>
</div>
<div class="card-actions">
<ha-progress-button
appearance="filled"
@click=${this._updateHomeName}
.disabled=${disabled}
>
${this.hass.localize("ui.common.save")}
</ha-progress-button>
</div>
</ha-card>
`;
}
private _renderLocationCard(canEdit: boolean): TemplateResult {
const hasHomeZone = "zone.home" in this.hass.states;
return html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.core.section.core.location_card.header"
)}
>
${hasHomeZone
? html`
<div class="card-content">
<ha-map
.hass=${this.hass}
.entities=${["zone.home"]}
.zoom=${14}
.autoFit=${true}
.fitZones=${true}
.themeMode=${"auto"}
.renderPassive=${false}
.interactiveZones=${false}
class="map-preview"
></ha-map>
</div>
`
: nothing}
<div class="card-actions">
<ha-button
appearance="filled"
@click=${this._editLocation}
.disabled=${!canEdit}
>
${this.hass.localize("ui.common.edit")}
</ha-button>
</div>
</ha-card>
`;
}
private _renderRegionalSettingsCard(canEdit: boolean): TemplateResult {
const disabled = this._submittingRegional || !canEdit;
return html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.core.section.core.regional_settings_card.header"
)}
>
<div class="card-content">
<ha-timezone-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.time_zone"
)}
name="timeZone"
.disabled=${disabled}
.value=${this._timeZone}
@value-changed=${this._handleValueChanged}
hide-clear-icon
></ha-timezone-picker>
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation"
)}
name="elevation"
type="number"
.disabled=${disabled}
.value=${this._elevation}
.suffix=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)}
@change=${this._handleChange}
>
</ha-textfield>
<div>
<div>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system"
)}
</div>
<ha-formfield
.label=${html`
<span style="font-size: 14px">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.metric_example"
)}
</span>
<div style="color: var(--secondary-text-color)">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_metric"
)}
</div>
`}
>
<ha-radio
name="unit_system"
value="metric"
.checked=${this._unitSystem === "metric"}
@change=${this._unitSystemChanged}
.disabled=${disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`
<span style="font-size: 14px">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.us_customary_example"
)}
</span>
<div style="color: var(--secondary-text-color)">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_us_customary"
)}
</div>
`}
>
<ha-radio
name="unit_system"
value="us_customary"
.checked=${this._unitSystem === "us_customary"}
@change=${this._unitSystemChanged}
.disabled=${disabled}
></ha-radio>
</ha-formfield>
${this._unitSystem !== this._configuredUnitSystem()
? html`
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_label"
)}
>
<ha-checkbox
.checked=${this._updateUnits}
.disabled=${this._submittingRegional}
@change=${this._updateUnitsChanged}
></ha-checkbox>
</ha-formfield>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_text_1"
)}
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_text_2"
)}
<br /><br />
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_text_3"
)}
</div>
`
: ""}
</div>
<div>
<ha-currency-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.currency"
)}
name="currency"
.disabled=${disabled}
.value=${this._currency}
@value-changed=${this._handleValueChanged}
></ha-currency-picker>
<a
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
target="_blank"
rel="noopener noreferrer"
class="find-value"
>${this.hass.localize(
"ui.panel.config.core.section.core.core_config.find_currency_value"
)}</a
>
</div>
<ha-country-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.country"
)}
name="country"
.disabled=${disabled}
.value=${this._country}
@value-changed=${this._handleValueChanged}
></ha-country-picker>
<ha-language-picker
.hass=${this.hass}
native-name
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.language"
)}
name="language"
.value=${this._language}
.disabled=${disabled}
@closed=${stopPropagation}
@value-changed=${this._handleValueChanged}
>
</ha-language-picker>
</div>
<div class="card-actions">
<ha-progress-button
appearance="filled"
@click=${this._updateRegionalSettings}
.disabled=${disabled}
>
${this.hass.localize("ui.common.save")}
</ha-progress-button>
</div>
</ha-card>
`;
}
private _configuredUnitSystem() {
return this.hass.config.unit_system.temperature === UNIT_C
? "metric"
@@ -351,6 +297,12 @@ class HaConfigSectionGeneral extends LitElement {
this._timeZone = this.hass.config.time_zone || "Etc/GMT";
this._name = this.hass.config.location_name;
this._updateUnits = true;
if (window.location.hash === "#ai-task") {
this._aiTaskPref.updateComplete.then(() => {
this._aiTaskPref.scrollIntoView();
});
}
}
private _handleValueChanged(ev: ValueChangedEvent<string>) {
@@ -373,31 +325,7 @@ class HaConfigSectionGeneral extends LitElement {
this._updateUnits = (ev.target as HaCheckbox).checked;
}
private async _updateHomeName(ev: CustomEvent) {
const button = ev.target as HaProgressButton;
if (button.progress) {
return;
}
button.progress = true;
this._submittingName = true;
this._error = undefined;
try {
await saveCoreConfig(this.hass, {
location_name: this._name,
});
button.actionSuccess();
} catch (err: any) {
button.actionError();
this._error = err.message;
} finally {
button.progress = false;
this._submittingName = false;
}
}
private async _updateRegionalSettings(ev: CustomEvent) {
private async _updateEntry(ev: CustomEvent) {
const button = ev.target as HaProgressButton;
if (button.progress) {
return;
@@ -422,8 +350,6 @@ class HaConfigSectionGeneral extends LitElement {
}
}
button.progress = true;
this._submittingRegional = true;
this._error = undefined;
let locationConfig;
@@ -438,13 +364,14 @@ class HaConfigSectionGeneral extends LitElement {
try {
await saveCoreConfig(this.hass, {
time_zone: this._timeZone,
currency: this._currency,
elevation: Number(this._elevation),
unit_system: this._unitSystem,
update_units: this._updateUnits && unitSystemChanged,
currency: this._currency,
country: this._country,
time_zone: this._timeZone,
location_name: this._name,
language: this._language,
country: this._country,
...locationConfig,
});
button.actionSuccess();
@@ -453,7 +380,6 @@ class HaConfigSectionGeneral extends LitElement {
this._error = err.message;
} finally {
button.progress = false;
this._submittingRegional = false;
}
}
@@ -465,39 +391,48 @@ class HaConfigSectionGeneral extends LitElement {
haStyle,
css`
.content {
padding: var(--ha-space-7) var(--ha-space-5) 0;
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
ha-card {
ha-card,
ai-task-pref {
max-width: 600px;
margin: 0 auto var(--ha-space-6);
margin: 0 auto;
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
}
ha-card,
ai-task-pref {
margin-bottom: 24px;
}
.card-content {
display: flex;
justify-content: space-between;
flex-direction: column;
padding: 16px 16px 0 16px;
}
.card-actions {
text-align: right;
height: 48px;
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 16px;
}
.card-content > * {
display: block;
margin-top: 16px;
}
.card-content > *:not(:first-child) {
margin-top: var(--ha-space-4);
}
.card-actions {
display: flex;
justify-content: flex-end;
ha-select {
display: block;
}
a.find-value {
margin-top: var(--ha-space-2);
margin-top: 8px;
display: inline-block;
}
.map-preview {
height: 200px;
width: 100%;
display: block;
border-radius: var(--ha-card-border-radius, 8px);
overflow: hidden;
}
`,
];
}
@@ -87,9 +87,7 @@ class HaConfigSystemNavigation extends LitElement {
description = this._storageInfo
? this.hass.localize("ui.panel.config.storage.description", {
percent_used: `${Math.round(
((this._storageInfo.total - this._storageInfo.free) /
this._storageInfo.total) *
100
(this._storageInfo.used / this._storageInfo.total) * 100
)}${blankBeforePercent(this.hass.locale)}%`,
free_space: `${this._storageInfo.free} GB`,
})
@@ -5,8 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-wa-dialog";
import { createCloseHeading } from "../../../../components/ha-dialog";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@@ -22,21 +21,14 @@ export class DialogJoinBeta
@state() private _dialogParams?: JoinBetaDialogParams;
@state() private _open = false;
public showDialog(dialogParams: JoinBetaDialogParams): void {
this._dialogParams = dialogParams;
this._open = true;
}
public closeDialog() {
this._open = false;
return true;
}
private _dialogClosed() {
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
protected render() {
@@ -45,11 +37,13 @@ export class DialogJoinBeta
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize("ui.dialogs.join_beta_channel.title")}
@closed=${this._dialogClosed}
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.join_beta_channel.title")
)}
>
<ha-alert alert-type="warning">
${this.hass.localize("ui.dialogs.join_beta_channel.backup")}
@@ -73,19 +67,17 @@ export class DialogJoinBeta
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this._cancel}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._join}>
${this.hass.localize("ui.dialogs.join_beta_channel.join")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
<ha-button
appearance="plain"
slot="primaryAction"
@click=${this._cancel}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._join}>
${this.hass.localize("ui.dialogs.join_beta_channel.join")}
</ha-button>
</ha-dialog>
`;
}
@@ -9,11 +9,11 @@ import type {
LocalizeFunc,
LocalizeKeys,
} from "../../../common/translations/localize";
import "../../../components/ha-wa-dialog";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/search-input";
import type { LovelaceConfig } from "../../../data/lovelace/config/types";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyleScrollbar } from "../../../resources/styles";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { generateDefaultView } from "../../lovelace/views/default-view";
import "./dashboard-card";
@@ -65,7 +65,7 @@ const STRATEGIES = [
class DialogNewDashboard extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@state() private _opened = false;
@state() private _params?: NewDashboardDialogParams;
@@ -77,7 +77,7 @@ class DialogNewDashboard extends LitElement implements HassDialog {
})[] = [];
public showDialog(params: NewDashboardDialogParams): void {
this._open = true;
this._opened = true;
this._params = params;
this._localizedStrategies = STRATEGIES.map((strategy) => ({
...strategy,
@@ -89,13 +89,12 @@ class DialogNewDashboard extends LitElement implements HassDialog {
}
public closeDialog() {
this._open = false;
return true;
}
private _dialogClosed(): void {
if (this._opened) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._opened = false;
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
private _generateDefaultConfig = memoizeOne(
@@ -105,100 +104,98 @@ class DialogNewDashboard extends LitElement implements HassDialog {
);
protected render() {
if (!this._params) {
if (!this._opened) {
return nothing;
}
const defaultConfig = this._generateDefaultConfig(this.hass.localize);
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
flexcontent
width="large"
header-title=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.header`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.header`
)
)}
@closed=${this._dialogClosed}
>
<div class="content-wrapper">
<search-input
autofocus
.hass=${this.hass}
.label=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.search_dashboards`
)}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
></search-input>
<div class="content ha-scrollbar">
${this._filter
? html`
<div class="cards-container">
${this._filterStrategies(
this._localizedStrategies,
this._filter
).map(
(strategy) => html`
<dashboard-card
.name=${strategy.localizedName}
.description=${strategy.localizedDescription}
.img=${this.hass.themes.darkMode
? strategy.images.dark
: strategy.images.light}
.alt=${strategy.localizedName}
@click=${this._selected}
.strategy=${strategy.type}
></dashboard-card>
`
<search-input
.hass=${this.hass}
.label=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.search_dashboards`
)}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
></search-input>
<div class="content">
${this._filter
? html`
<div class="cards-container">
${this._filterStrategies(
this._localizedStrategies,
this._filter
).map(
(strategy) => html`
<dashboard-card
.name=${strategy.localizedName}
.description=${strategy.localizedDescription}
.img=${this.hass.themes.darkMode
? strategy.images.dark
: strategy.images.light}
.alt=${strategy.localizedName}
@click=${this._selected}
.strategy=${strategy.type}
></dashboard-card>
`
)}
</div>
`
: html`
<div class="cards-container">
<dashboard-card
.name=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty`
)}
.description=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty_description`
)}
.img=${this.hass.themes.darkMode
? "/static/images/dashboard-options/dark/icon-dashboard-new.svg"
: "/static/images/dashboard-options/light/icon-dashboard-new.svg"}
.alt=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty`
)}
@click=${this._selected}
.config=${defaultConfig}
></dashboard-card>
</div>
<div class="cards-container">
<div class="cards-container-header">
${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.heading.default`
)}
</div>
`
: html`
<div class="cards-container">
<dashboard-card
.name=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty`
)}
.description=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty_description`
)}
.img=${this.hass.themes.darkMode
? "/static/images/dashboard-options/dark/icon-dashboard-new.svg"
: "/static/images/dashboard-options/light/icon-dashboard-new.svg"}
.alt=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty`
)}
@click=${this._selected}
.config=${defaultConfig}
></dashboard-card>
</div>
<div class="cards-container">
<div class="cards-container-header">
${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.heading.default`
)}
</div>
${this._localizedStrategies.map(
(strategy) => html`
<dashboard-card
.name=${strategy.localizedName}
.description=${strategy.localizedDescription}
.img=${this.hass.themes.darkMode
? strategy.images.dark
: strategy.images.light}
.alt=${strategy.localizedName}
@click=${this._selected}
.strategy=${strategy.type}
></dashboard-card>
`
)}
</div>
`}
</div>
${this._localizedStrategies.map(
(strategy) => html`
<dashboard-card
.name=${strategy.localizedName}
.description=${strategy.localizedDescription}
.img=${this.hass.themes.darkMode
? strategy.images.dark
: strategy.images.light}
.alt=${strategy.localizedName}
@click=${this._selected}
.strategy=${strategy.type}
></dashboard-card>
`
)}
</div>
`}
</div>
</ha-wa-dialog>
</ha-dialog>
`;
}
@@ -256,16 +253,33 @@ class DialogNewDashboard extends LitElement implements HassDialog {
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
haStyle,
haStyleDialog,
css`
ha-wa-dialog {
@media all and (max-width: 450px), all and (max-height: 500px) {
/* overrule the ha-style-dialog max-height on small screens */
ha-dialog {
--mdc-dialog-max-height: 100%;
height: 100%;
}
}
@media all and (min-width: 850px) {
ha-dialog {
--mdc-dialog-min-width: 845px;
--mdc-dialog-min-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
--mdc-dialog-max-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
}
}
ha-dialog {
--mdc-dialog-max-width: 845px;
--dialog-content-padding: 0;
--dialog-z-index: 6;
--ha-dialog-min-height: 60svh;
}
ha-wa-dialog::part(body) {
overflow: hidden;
min-height: 0;
}
.cards-container-header {
font-size: var(--ha-font-size-l);
@@ -301,17 +315,8 @@ class DialogNewDashboard extends LitElement implements HassDialog {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
margin-top: 20px;
}
.content-wrapper {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.content {
padding: 0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6);
flex: 1;
min-height: 0;
overflow: auto;
padding: 0 24px 0 24px;
}
`,
];
@@ -2,7 +2,6 @@ import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-card";
import "../../../../components/ha-button";
import "../../../../components/ha-md-list";
import "../../../../components/entity/ha-entity-picker";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles";
@@ -23,6 +22,8 @@ import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_regi
class HaPanelDevDebug extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@state() private _entityId?: string;
protected render() {
@@ -33,14 +34,14 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
"ui.panel.config.developer-tools.tabs.debug.title"
)}
>
<ha-md-list>
<ha-debug-connection-row
.hass=${this.hass}
></ha-debug-connection-row>
<ha-debug-disable-view-transition-row
.hass=${this.hass}
></ha-debug-disable-view-transition-row>
</ha-md-list>
<ha-debug-connection-row
.hass=${this.hass}
.narrow=${this.narrow}
></ha-debug-connection-row>
<ha-debug-disable-view-transition-row
.hass=${this.hass}
.narrow=${this.narrow}
></ha-debug-disable-view-transition-row>
</ha-card>
<ha-card
.header=${this.hass.localize(
@@ -127,11 +128,6 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
max-width: 600px;
margin: 0 auto;
}
ha-md-list {
padding-top: 0;
padding-bottom: 0;
background: none;
}
`,
];
}
@@ -1,7 +1,7 @@
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-settings-row";
import "../../../../components/ha-switch";
import type { HaSwitch } from "../../../../components/ha-switch";
import type { HomeAssistant } from "../../../../types";
@@ -11,25 +11,26 @@ import { storeState } from "../../../../util/ha-pref-storage";
class HaDebugConnectionRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
protected render(): TemplateResult {
return html`
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.debug_connection.title"
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
)}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.debug_connection.description"
)}</span
>
)}
</span>
<ha-switch
slot="end"
.checked=${this.hass.debugConnection}
@change=${this._checkedChanged}
></ha-switch>
</ha-md-list-item>
</ha-settings-row>
`;
}
@@ -3,7 +3,7 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { storage } from "../../../../common/decorators/storage";
import { setViewTransitionDisabled } from "../../../../common/util/view-transition";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-settings-row";
import "../../../../components/ha-switch";
import type { HaSwitch } from "../../../../components/ha-switch";
import type { HomeAssistant } from "../../../../types";
@@ -12,28 +12,29 @@ import type { HomeAssistant } from "../../../../types";
class HaDebugDisableViewTransitionRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@storage({ key: "disableViewTransition", state: true, subscribe: false })
private _disabled = false;
protected render(): TemplateResult {
return html`
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.disable_view_transition.title"
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
)}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.disable_view_transition.description"
)}</span
>
)}
</span>
<ha-switch
slot="end"
.checked=${this._disabled}
@change=${this._checkedChanged}
></ha-switch>
</ha-md-list-item>
</ha-settings-row>
`;
}
@@ -10,10 +10,10 @@ import {
mdiUnfoldMoreHorizontal,
} from "@mdi/js";
import "@home-assistant/webawesome/dist/components/divider/divider";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
@@ -28,11 +28,15 @@ import type {
SortingDirection,
} from "../../../../components/data-table/ha-data-table";
import { showDataTableSettingsDialog } from "../../../../components/data-table/show-dialog-data-table-settings";
import "@home-assistant/webawesome/dist/components/divider/divider";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import type { HaMdMenu } from "../../../../components/ha-md-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/search-input-outlined";
import type {
StatisticsMetaData,
@@ -108,8 +112,20 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
@query("ha-data-table", true) private _dataTable!: HaDataTable;
@query("#group-by-menu") private _groupByMenu!: HaMdMenu;
@query("#sort-by-menu") private _sortByMenu!: HaMdMenu;
@query("search-input-outlined") private _searchInput!: HTMLElement;
private _toggleGroupBy() {
this._groupByMenu.open = !this._groupByMenu.open;
}
private _toggleSortBy() {
this._sortByMenu.open = !this._sortByMenu.open;
}
protected firstUpdated() {
this._validateStatistics();
}
@@ -262,106 +278,37 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
const sortByMenu = Object.values(columns).find((col) => col.sortable)
? html`
<ha-dropdown @wa-select=${this._handleSortBy}>
<ha-assist-chip
slot="trigger"
.label=${localize("ui.components.subpage-data-table.sort_by", {
sortColumn: this._sortColumn
? ` ${columns[this._sortColumn]?.title || columns[this._sortColumn]?.label}` ||
""
: "",
})}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${Object.entries(columns).map(([id, column]) =>
column.sortable
? html`
<ha-dropdown-item
.value=${id}
.selected=${id === this._sortColumn}
>
${this._sortColumn === id
? html`
<ha-svg-icon
slot="details"
.path=${this._sortDirection === "desc"
? mdiArrowDown
: mdiArrowUp}
></ha-svg-icon>
`
: nothing}
${column.title || column.label}
</ha-dropdown-item>
`
: nothing
)}
</ha-dropdown>
<ha-assist-chip
.label=${localize("ui.components.subpage-data-table.sort_by", {
sortColumn: this._sortColumn
? ` ${columns[this._sortColumn]?.title || columns[this._sortColumn]?.label}` ||
""
: "",
})}
id="sort-by-anchor"
@click=${this._toggleSortBy}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
`
: nothing;
const groupByMenu = Object.values(columns).find((col) => col.groupable)
? html`
<ha-dropdown @wa-select=${this._handleOverflowGroupBy}>
<ha-assist-chip
slot="trigger"
.label=${localize("ui.components.subpage-data-table.group_by", {
groupColumn: this._groupColumn
? ` ${columns[this._groupColumn].title || columns[this._groupColumn].label}`
: "",
})}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${Object.entries(columns).map(([id, column]) =>
column.groupable
? html`
<ha-dropdown-item
.value=${id}
.selected=${id === this._groupColumn}
>
${column.title || column.label}
</ha-dropdown-item>
`
: nothing
)}
<ha-dropdown-item
value="none"
.selected=${this._groupColumn === undefined}
>
${localize("ui.components.subpage-data-table.dont_group_by")}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item
value="collapse_all"
@click=${this._collapseAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
slot="icon"
.path=${mdiUnfoldLessHorizontal}
></ha-svg-icon>
${localize(
"ui.components.subpage-data-table.collapse_all_groups"
)}
</ha-dropdown-item>
<ha-dropdown-item
@click=${this._expandAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
slot="icon"
.path=${mdiUnfoldMoreHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.expand_all_groups")}
</ha-dropdown-item>
</ha-dropdown>
<ha-assist-chip
.label=${localize("ui.components.subpage-data-table.group_by", {
groupColumn: this._groupColumn
? ` ${columns[this._groupColumn].title || columns[this._groupColumn].label}`
: "",
})}
id="group-by-anchor"
@click=${this._toggleGroupBy}
>
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon
></ha-assist-chip>
`
: nothing;
@@ -470,7 +417,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
.hiddenColumns=${this.hiddenColumns}
@row-click=${this._rowClicked}
@selection-changed=${this._handleSelectionChanged}
@sorting-changed=${this._handleTableSortingChanged}
>
${!this.narrow
? html`
@@ -488,6 +434,82 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
</div>`}
</ha-data-table>
</div>
<ha-md-menu
anchor="group-by-anchor"
id="group-by-menu"
positioning="fixed"
>
${Object.entries(columns).map(([id, column]) =>
column.groupable
? html`
<ha-md-menu-item
.value=${id}
@click=${this._handleGroupBy}
.selected=${id === this._groupColumn}
class=${classMap({ selected: id === this._groupColumn })}
>
${column.title || column.label}
</ha-md-menu-item>
`
: nothing
)}
<ha-md-menu-item
.value=${undefined}
@click=${this._handleGroupBy}
.selected=${this._groupColumn === undefined}
class=${classMap({ selected: this._groupColumn === undefined })}
>
${localize("ui.components.subpage-data-table.dont_group_by")}
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
@click=${this._collapseAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
slot="start"
.path=${mdiUnfoldLessHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.collapse_all_groups")}
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._expandAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
slot="start"
.path=${mdiUnfoldMoreHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.expand_all_groups")}
</ha-md-menu-item>
</ha-md-menu>
<ha-md-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
${Object.entries(columns).map(([id, column]) =>
column.sortable
? html`
<ha-md-menu-item
.value=${id}
@click=${this._handleSortBy}
keep-open
.selected=${id === this._sortColumn}
class=${classMap({ selected: id === this._sortColumn })}
>
${this._sortColumn === id
? html`
<ha-svg-icon
slot="end"
.path=${this._sortDirection === "desc"
? mdiArrowDown
: mdiArrowUp}
></ha-svg-icon>
`
: nothing}
${column.title || column.label}
</ha-md-menu-item>
`
: nothing
)}
</ha-md-menu>
`;
}
@@ -504,17 +526,8 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
this._selected = ev.detail.value;
}
private _handleTableSortingChanged(
ev: CustomEvent<{ column: string; direction: SortingDirection }>
) {
const { column, direction } = ev.detail;
this._sortColumn = column;
this._sortDirection = direction;
}
private _handleSortBy(ev: HaDropdownSelectEvent) {
ev.preventDefault(); // keep dropdown open
const columnId = ev.detail.item.value;
private _handleSortBy(ev) {
const columnId = ev.currentTarget.value;
if (!this._sortDirection || this._sortColumn !== columnId) {
this._sortDirection = "asc";
} else if (this._sortDirection === "asc") {
@@ -525,29 +538,11 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
this._sortColumn = columnId;
}
private _handleOverflowGroupBy = (ev: HaDropdownSelectEvent) => {
const action = ev.detail.item.value;
private _handleGroupBy(ev) {
this._setGroupColumn(ev.currentTarget.value);
}
if (!action) {
return;
}
switch (action) {
case "collapse_all":
this._collapseAllGroups();
return;
case "expand_all":
this._expandAllGroups();
return;
case "none":
this._setGroupColumn();
return;
default:
this._setGroupColumn(action);
}
};
private _setGroupColumn(columnId?: string) {
private _setGroupColumn(columnId: string) {
this._groupColumn = columnId;
}
@@ -802,6 +797,8 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
--dialog-content-padding: 0;
}
#sort-by-anchor,
#group-by-anchor,
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
@@ -30,12 +30,12 @@ import "../../../components/ha-labels-picker";
import "../../../components/ha-list-item";
import "../../../components/ha-radio";
import "../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../components/ha-select";
import "../../../components/ha-settings-row";
import "../../../components/ha-state-icon";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-textfield";
import type { HaSelectSelectEvent } from "../../../components/ha-select";
import {
CAMERA_ORIENTATIONS,
CAMERA_SUPPORT_STREAM,
@@ -434,15 +434,19 @@ export class EntityRegistrySettingsEditor extends LitElement {
>
<ha-dropdown-item
value="switch"
.selected=${this._switchAsDomain === "switch" &&
(!this._deviceClass || this._deviceClass === "switch")}
class=${this._switchAsDomain === "switch" &&
(!this._deviceClass || this._deviceClass === "switch")
? "selected"
: ""}
>
${domainToName(this.hass.localize, "switch")}
</ha-dropdown-item>
<ha-dropdown-item
value="outlet"
.selected=${this._switchAsDomain === "switch" &&
this._deviceClass === "outlet"}
class=${this._switchAsDomain === "switch" &&
this._deviceClass === "outlet"
? "selected"
: ""}
>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_classes.switch.outlet"
@@ -456,7 +460,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
(entry) => html`
<ha-dropdown-item
.value=${entry.domain}
.selected=${this._switchAsDomain === entry.domain}
class=${this._switchAsDomain === entry.domain
? "selected"
: ""}
>
${entry.label}
</ha-dropdown-item>
@@ -473,13 +479,13 @@ export class EntityRegistrySettingsEditor extends LitElement {
>
<ha-dropdown-item
value="switch"
.selected=${this._switchAsDomain === "switch"}
class=${this._switchAsDomain === "switch" ? "selected" : ""}
>
${domainToName(this.hass.localize, "switch")}
</ha-dropdown-item>
<ha-dropdown-item
.value=${domain}
.selected=${this._switchAsDomain === domain}
class=${this._switchAsDomain === domain ? "selected" : ""}
>
${domainToName(this.hass.localize, domain)}
</ha-dropdown-item>
@@ -493,7 +499,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
: html`
<ha-dropdown-item
.value=${entry.domain}
.selected=${this._switchAsDomain === entry.domain}
class=${this._switchAsDomain === entry.domain
? "selected"
: ""}
>
${entry.label}
</ha-dropdown-item>
@@ -543,7 +551,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
(entry) => html`
<ha-dropdown-item
.value=${entry.deviceClass}
.selected=${entry.deviceClass === this._deviceClass}
class=${entry.deviceClass === this._deviceClass
? "selected"
: ""}
>
${entry.label}
</ha-dropdown-item>
@@ -561,7 +571,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
(entry) => html`
<ha-dropdown-item
.value=${entry.deviceClass}
.selected=${entry.deviceClass === this._deviceClass}
class=${entry.deviceClass === this._deviceClass
? "selected"
: ""}
>
${entry.label}
</ha-dropdown-item>
-13
View File
@@ -27,7 +27,6 @@ import {
mdiScriptText,
mdiShape,
mdiSofa,
mdiStarFourPoints,
mdiTextBoxOutline,
mdiTools,
mdiUpdate,
@@ -119,7 +118,6 @@ export const configSections: Record<string, PageNavigation[]> = {
path: "/config/matter",
iconPath:
"M7.228375 6.41685c0.98855 0.80195 2.16365 1.3412 3.416275 1.56765V1.30093l1.3612 -0.7854275 1.360125 0.7854275V7.9845c1.252875 -0.226675 2.4283 -0.765875 3.41735 -1.56765l2.471225 1.4293c-4.019075 3.976275 -10.490025 3.976275 -14.5091 0l2.482925 -1.4293Zm3.00335 17.067575c1.43325 -5.47035 -1.8052 -11.074775 -7.2604 -12.564675v2.859675c1.189125 0.455 2.244125 1.202875 3.0672 2.174275L0.25 19.2955v1.5719l1.3611925 0.781175L7.39865 18.3068c0.430175 1.19825 0.550625 2.48575 0.35015 3.743l2.482925 1.434625ZM21.034 10.91975c-5.452225 1.4932 -8.6871 7.09635 -7.254025 12.564675l2.47655 -1.43035c-0.200025 -1.257275 -0.079575 -2.544675 0.35015 -3.743025l5.7832 3.337525L23.75 20.86315V19.2955L17.961475 15.9537c0.8233 -0.97115 1.878225 -1.718975 3.0672 -2.174275l0.005325 -2.859675Z",
iconViewBox: "0 1 24 24",
iconColor: "#2458B3",
component: "matter",
translationKey: "matter",
@@ -400,13 +398,6 @@ export const configSections: Record<string, PageNavigation[]> = {
iconPath: mdiShape,
iconColor: "#f1c447",
},
{
path: "/config/ai-tasks",
translationKey: "ai_tasks",
iconPath: mdiStarFourPoints,
iconColor: "#8B69E3",
core: true,
},
{
path: "/config/labs",
translationKey: "labs",
@@ -610,10 +601,6 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
tag: "ha-config-labs",
load: () => import("./labs/ha-config-labs"),
},
"ai-tasks": {
tag: "ha-config-section-ai-tasks",
load: () => import("./core/ha-config-section-ai-tasks"),
},
zha: {
tag: "zha-config-dashboard-router",
load: () =>
@@ -12,11 +12,10 @@ import "../../../components/chart/ha-chart-base";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-fade-in";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-settings-row";
import type { ConfigEntry } from "../../../data/config_entries";
import { subscribeConfigEntries } from "../../../data/config_entries";
import type {
@@ -366,7 +365,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
})}
</ha-card>`
: nothing}
${isComponentLoaded(this.hass, "hardware")
${this._systemStatusData
? html`<ha-card outlined>
<div class="header">
<div class="title">
@@ -375,25 +374,16 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
)}
</div>
<div class="value">
${this._systemStatusData
? html`${this._systemStatusData
.cpu_percent}${blankBeforePercent(
this.hass.locale
)}%`
: "-"}
${this._systemStatusData.cpu_percent ||
"-"}${blankBeforePercent(this.hass.locale)}%
</div>
</div>
<div class="card-content loading-container">
<div class="card-content">
<ha-chart-base
.hass=${this.hass}
.data=${this._getChartData(this._cpuEntries)}
.options=${this._chartOptions}
></ha-chart-base>
${!this._systemStatusData
? html` <ha-fade-in delay="1000" class="loading-overlay">
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>`
: nothing}
</div>
</ha-card>
<ha-card outlined>
@@ -402,38 +392,37 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.hardware.memory")}
</div>
<div class="value">
${this._systemStatusData
? html`${round(
this._systemStatusData.memory_used_mb / 1024,
1
)}
GB /
${round(
(this._systemStatusData.memory_used_mb +
this._systemStatusData.memory_free_mb) /
1024,
0
)}
GB`
: "-"}
${round(this._systemStatusData.memory_used_mb / 1024, 1)}
GB /
${round(
(this._systemStatusData.memory_used_mb! +
this._systemStatusData.memory_free_mb!) /
1024,
0
)}
GB
</div>
</div>
<div class="card-content loading-container">
<div class="card-content">
<ha-chart-base
.hass=${this.hass}
.data=${this._getChartData(this._memoryEntries)}
.options=${this._chartOptions}
></ha-chart-base>
${!this._systemStatusData
? html`
<ha-fade-in delay="1000" class="loading-overlay">
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>
`
: nothing}
</div>
</ha-card>`
: nothing}
: isComponentLoaded(this.hass, "hardware")
? html`<ha-card outlined>
<div class="card-content">
<ha-alert alert-type="info">
<ha-spinner slot="icon"></ha-spinner>
${this.hass.localize(
"ui.panel.config.hardware.loading_system_data"
)}
</ha-alert>
</div>
</ha-card>`
: nothing}
</div>
</hass-subpage>
`;
@@ -513,22 +502,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
flex-direction: column;
padding: 16px;
}
.loading-container {
position: relative;
}
.loading-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(var(--rgb-card-background-color), 0.75);
display: flex;
justify-content: center;
align-items: center;
}
.card-content img {
max-width: 300px;
margin: auto;
@@ -575,6 +548,10 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
ha-alert {
--ha-alert-icon-size: 24px;
}
ha-alert ha-spinner {
--ha-spinner-size: 24px;
}
`,
];
}
+77 -100
View File
@@ -1,22 +1,21 @@
import { mdiAlertOutline } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { stringCompare } from "../../../common/string/compare";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-list";
import "../../../components/ha-button";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import "../../../components/ha-wa-dialog";
import "../../../components/search-input";
import { getConfigFlowHandlers } from "../../../data/config_flow";
import { createCounter } from "../../../data/counter";
import { createInputBoolean } from "../../../data/input_boolean";
@@ -28,7 +27,6 @@ import { createInputText } from "../../../data/input_text";
import {
domainToName,
fetchIntegrationManifest,
type IntegrationManifest,
} from "../../../data/integration";
import { createSchedule } from "../../../data/schedule";
import { createTimer } from "../../../data/timer";
@@ -105,7 +103,7 @@ export class DialogHelperDetail extends LitElement {
@state() private _item?: Helper;
@state() private _open = false;
@state() private _opened = false;
@state() private _domain?: string;
@@ -113,18 +111,14 @@ export class DialogHelperDetail extends LitElement {
@state() private _submitting = false;
@query(".form") private _form?: HTMLDivElement;
@state() private _helperFlows?: string[];
@state() private _loading = false;
@state() private _filter?: string;
private _pendingConfigFlow?: {
startFlowHandler: string;
manifest: IntegrationManifest;
dialogClosedCallback?: ShowDialogHelperDetailParams["dialogClosedCallback"];
};
private _params?: ShowDialogHelperDetailParams;
public async showDialog(params: ShowDialogHelperDetailParams): Promise<void> {
@@ -134,47 +128,29 @@ export class DialogHelperDetail extends LitElement {
if (this._domain && this._domain in HELPERS) {
await HELPERS[this._domain].import();
}
this._open = true;
this._opened = true;
await this.updateComplete;
this.hass.loadFragmentTranslation("config");
const flows = await getConfigFlowHandlers(this.hass, ["helper"]);
await this.hass.loadBackendTranslation("title", flows, true);
// Ensure the titles are loaded before we render the flows.
this._helperFlows = flows;
await this.updateComplete;
await this._focusSearchInput();
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._open = false;
this._opened = false;
this._error = undefined;
this._domain = undefined;
this._params = undefined;
this._filter = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
if (this._pendingConfigFlow) {
const pendingConfigFlow = this._pendingConfigFlow;
this._pendingConfigFlow = undefined;
showConfigFlowDialog(this, {
startFlowHandler: pendingConfigFlow.startFlowHandler,
manifest: pendingConfigFlow.manifest,
dialogClosedCallback: pendingConfigFlow.dialogClosedCallback,
});
}
}
protected render() {
if (!this._params) {
if (!this._opened) {
return nothing;
}
let content: TemplateResult;
let footer: TemplateResult | typeof nothing = nothing;
if (this._domain) {
content = html`
@@ -184,30 +160,25 @@ export class DialogHelperDetail extends LitElement {
hass: this.hass,
item: this._item,
new: true,
autofocus: true,
})}
</div>
`;
footer = html`
<ha-dialog-footer slot="footer">
${this._params?.domain
? nothing
: html`<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this._goBack}
.disabled=${this._submitting}
>
${this.hass!.localize("ui.common.back")}
</ha-button>`}
<ha-button
slot="primaryAction"
@click=${this._createItem}
.disabled=${this._submitting}
>
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
</ha-button>
</ha-dialog-footer>
<ha-button
slot="primaryAction"
@click=${this._createItem}
.disabled=${this._submitting}
>
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
</ha-button>
${this._params?.domain
? nothing
: html`<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this._goBack}
.disabled=${this._submitting}
>
${this.hass!.localize("ui.common.back")}
</ha-button>`}
`;
} else if (this._loading || this._helperFlows === undefined) {
content = html`<ha-spinner></ha-spinner>`;
@@ -220,8 +191,8 @@ export class DialogHelperDetail extends LitElement {
content = html`
<search-input
autofocus
.hass=${this.hass}
dialogInitialFocus="true"
.filter=${this._filter}
@value-changed=${this._filterChanged}
.label=${this.hass.localize(
@@ -236,6 +207,7 @@ export class DialogHelperDetail extends LitElement {
"ui.panel.config.helpers.dialog.create_helper"
)}
rootTabbable
dialogInitialFocus
>
${items.map(([domain, label]) => {
// Only OG helpers need to be loaded prior adding one
@@ -283,28 +255,32 @@ export class DialogHelperDetail extends LitElement {
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this._domain
? this.hass.localize(
"ui.panel.config.helpers.dialog.create_platform",
{
platform:
(isHelperDomain(this._domain) &&
this.hass.localize(
`ui.panel.config.helpers.types.${
this._domain as HelperDomain
}`
)) ||
this._domain,
}
)
: this.hass.localize("ui.panel.config.helpers.dialog.create_helper")}
@closed=${this._dialogClosed}
<ha-dialog
open
@closed=${this.closeDialog}
class=${classMap({ "button-left": !this._domain })}
.hideActions=${!this._domain}
.heading=${createCloseHeading(
this.hass,
this._domain
? this.hass.localize(
"ui.panel.config.helpers.dialog.create_platform",
{
platform:
(isHelperDomain(this._domain) &&
this.hass.localize(
`ui.panel.config.helpers.types.${
this._domain as HelperDomain
}`
)) ||
this._domain,
}
)
: this.hass.localize("ui.panel.config.helpers.dialog.create_helper")
)}
>
${content} ${footer}
</ha-wa-dialog>
${content}
</ha-dialog>
`;
}
@@ -405,35 +381,26 @@ export class DialogHelperDetail extends LitElement {
} finally {
this._loading = false;
}
this._focusForm();
} else {
this._pendingConfigFlow = {
showConfigFlowDialog(this, {
startFlowHandler: domain,
manifest: await fetchIntegrationManifest(this.hass, domain),
dialogClosedCallback: this._params?.dialogClosedCallback,
};
dialogClosedCallback: this._params!.dialogClosedCallback,
});
this.closeDialog();
}
}
private async _goBack() {
private async _focusForm(): Promise<void> {
await this.updateComplete;
(this._form?.lastElementChild as HTMLElement).focus();
}
private _goBack() {
this._domain = undefined;
this._item = undefined;
this._error = undefined;
await this.updateComplete;
await this._focusSearchInput();
}
private async _focusSearchInput() {
const searchInput = this.shadowRoot?.querySelector("search-input") as
| (HTMLElement & { updateComplete?: Promise<unknown> })
| null;
if (!searchInput) {
return;
}
await searchInput.updateComplete;
searchInput.focus();
}
static get styles(): CSSResultGroup {
@@ -441,21 +408,31 @@ export class DialogHelperDetail extends LitElement {
haStyleScrollbar,
haStyleDialog,
css`
ha-wa-dialog {
ha-dialog.button-left {
--justify-action-buttons: flex-start;
}
ha-dialog {
--dialog-content-padding: 0;
--dialog-scroll-divider-color: transparent;
--mdc-dialog-max-height: 90vh;
}
@media all and (min-width: 550px) {
ha-dialog {
--mdc-dialog-min-width: 500px;
}
}
ha-icon-next {
width: var(--ha-space-6);
width: 24px;
}
ha-tooltip {
pointer-events: auto;
}
.form {
padding: var(--ha-space-6);
padding: 24px;
}
search-input {
display: block;
margin: 0 var(--ha-space-4) 0;
margin: 16px 16px 0;
}
ha-list {
height: calc(60vh - 184px);
@@ -3,8 +3,7 @@ import { html, LitElement, nothing } from "lit";
import memoizeOne from "memoize-one";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-wa-dialog";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-button";
import { haStyleDialog } from "../../../../resources/styles";
@@ -25,8 +24,6 @@ class DialogScheduleBlockInfo extends LitElement {
@state() private _params?: ScheduleBlockInfoDialogParams;
@state() private _open = false;
private _expand = false;
private _schema = memoizeOne((expand: boolean) => [
@@ -60,14 +57,9 @@ class DialogScheduleBlockInfo extends LitElement {
this._error = undefined;
this._data = params.block;
this._expand = !!params.block?.data;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._params = undefined;
this._data = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -79,17 +71,18 @@ class DialogScheduleBlockInfo extends LitElement {
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass!.localize(
"ui.dialogs.helper_settings.schedule.edit_schedule_block"
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass!.localize(
"ui.dialogs.helper_settings.schedule.edit_schedule_block"
)
)}
@closed=${this._dialogClosed}
>
<div>
<ha-form
autofocus
.hass=${this.hass}
.schema=${this._schema(this._expand)}
.data=${this._data}
@@ -98,20 +91,18 @@ class DialogScheduleBlockInfo extends LitElement {
@value-changed=${this._valueChanged}
></ha-form>
</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
@click=${this._deleteBlock}
appearance="filled"
variant="danger"
>
${this.hass!.localize("ui.common.delete")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._updateBlock}>
${this.hass!.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
<ha-button
slot="secondaryAction"
@click=${this._deleteBlock}
appearance="plain"
variant="danger"
>
${this.hass!.localize("ui.common.delete")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._updateBlock}>
${this.hass!.localize("ui.common.save")}
</ha-button>
</ha-dialog>
`;
}
@@ -369,6 +369,7 @@ export class BluetoothConfigDashboard extends LitElement {
padding: 24px 0 32px;
max-width: 600px;
margin: 0 auto;
direction: ltr;
}
ha-card {
margin-bottom: 16px;
@@ -323,9 +323,9 @@ class ZHAConfigDashboard extends LitElement {
const configEntries = await getConfigEntries(this.hass, {
domain: "zha",
});
this._configEntry = configEntries.find(
(entry) => entry.disabled_by === null && entry.source !== "ignore"
);
if (configEntries.length) {
this._configEntry = configEntries[0];
}
}
private async _fetchConfiguration(): Promise<void> {
+32 -72
View File
@@ -1,4 +1,3 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import {
mdiDelete,
mdiDevices,
@@ -21,16 +20,13 @@ import type {
RowClickedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-dropdown";
import type {
HaDropdown,
HaDropdownSelectEvent,
} from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import { renderLabelColorBadge } from "../../../components/ha-label-picker";
import "../../../components/ha-md-menu";
import type { HaMdMenu } from "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-svg-icon";
import type {
LabelRegistryEntry,
@@ -48,12 +44,12 @@ import {
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage-data-table";
import type { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "./show-dialog-label-detail";
import {
getCreatedAtTableColumn,
getModifiedAtTableColumn,
} from "../common/data-table-columns";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "./show-dialog-label-detail";
@customElement("ha-config-labels")
export class HaConfigLabels extends LitElement {
@@ -97,12 +93,10 @@ export class HaConfigLabels extends LitElement {
})
private _activeHiddenColumns?: string[];
@query("#overflow-menu") private _overflowMenu?: HaDropdown;
@query("#overflow-menu") private _overflowMenu?: HaMdMenu;
private _overflowLabel!: LabelRegistryEntry;
private _openingOverflow = false;
private _columns = memoizeOne((localize: LocalizeFunc, narrow: boolean) => {
const columns: DataTableColumnContainer<LabelRegistryEntry> = {
icon: {
@@ -178,27 +172,13 @@ export class HaConfigLabels extends LitElement {
return;
}
if (this._overflowMenu.anchorElement === ev.target) {
this._overflowMenu.anchorElement = undefined;
if (this._overflowMenu.open) {
this._overflowMenu.close();
return;
}
this._openingOverflow = true;
this._overflowMenu.anchorElement = ev.target;
this._overflowLabel = ev.target.selected;
this._overflowMenu.open = true;
};
private _overflowMenuOpened = () => {
this._openingOverflow = false;
};
private _overflowMenuClosed = () => {
// changing the anchorElement triggers a close event, ignore it
if (this._openingOverflow || !this._overflowMenu) {
return;
}
this._overflowMenu.anchorElement = undefined;
this._overflowMenu.anchorElement = ev.target;
this._overflowMenu.show();
};
protected firstUpdated(changedProperties: PropertyValues) {
@@ -244,30 +224,32 @@ export class HaConfigLabels extends LitElement {
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
<ha-dropdown
id="overflow-menu"
@wa-select=${this._handleOverflowAction}
@wa-after-show=${this._overflowMenuOpened}
@wa-after-hide=${this._overflowMenuClosed}
>
<ha-dropdown-item value="navigate-entities">
<ha-svg-icon slot="icon" .path=${mdiShape}></ha-svg-icon>
<ha-md-menu id="overflow-menu" positioning="fixed">
<ha-md-menu-item .clickAction=${this._navigateEntities}>
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
${this.hass.localize("ui.panel.config.entities.caption")}
</ha-dropdown-item>
<ha-dropdown-item value="navigate-devices">
<ha-svg-icon slot="icon" .path=${mdiDevices}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._navigateDevices}>
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
${this.hass.localize("ui.panel.config.devices.caption")}
</ha-dropdown-item>
<ha-dropdown-item value="navigate-automations">
<ha-svg-icon slot="icon" .path=${mdiRobot}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._navigateAutomations}>
<ha-svg-icon slot="start" .path=${mdiRobot}></ha-svg-icon>
${this.hass.localize("ui.panel.config.automation.caption")}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item variant="danger" value="remove">
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
class="warning"
.clickAction=${this._handleRemoveLabelClick}
>
<ha-svg-icon
slot="start"
class="warning"
.path=${mdiDelete}
></ha-svg-icon>
${this.hass.localize("ui.common.delete")}
</ha-dropdown-item>
</ha-dropdown>
</ha-md-menu-item>
</ha-md-menu>
`;
}
@@ -359,28 +341,6 @@ export class HaConfigLabels extends LitElement {
}
}
private _handleOverflowAction = (ev: HaDropdownSelectEvent) => {
const action = ev.detail.item.value;
if (!action) {
return;
}
switch (action) {
case "navigate-entities":
this._navigateEntities();
break;
case "navigate-devices":
this._navigateDevices();
break;
case "navigate-automations":
this._navigateAutomations();
break;
case "remove":
this._handleRemoveLabelClick();
break;
}
};
private _navigateEntities = () => {
navigate(
`/config/entities?historyBack=1&label=${this._overflowLabel.label_id}`
+7 -1
View File
@@ -169,7 +169,7 @@ class ErrorLogCard extends LitElement {
(boot) => html`
<ha-dropdown-item
.value=${`boot_${boot}`}
.selected=${boot === this._boot}
class=${boot === this._boot ? "selected" : ""}
>
${boot === 0
? localize("ui.panel.config.logs.current")
@@ -846,6 +846,12 @@ class ErrorLogCard extends LitElement {
.download-link {
color: var(--text-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);
}
`;
}
@@ -2,7 +2,8 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-card";
import { createCloseHeading } from "../../../components/ha-dialog";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import "./integrations-startup-time";
@@ -11,47 +12,44 @@ import "./integrations-startup-time";
class DialogIntegrationStartup extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@state() private _opened = false;
public showDialog(): void {
this._open = true;
this._opened = true;
}
public closeDialog() {
this._open = false;
}
private _dialogClosed(): void {
this._open = false;
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._open) {
if (!this._opened) {
return nothing;
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize(
"ui.panel.config.repairs.integration_startup_time"
<ha-dialog
open
hideActions
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.repairs.integration_startup_time")
)}
@closed=${this._dialogClosed}
@closed=${this.closeDialog}
>
<integrations-startup-time
.hass=${this.hass}
narrow
></integrations-startup-time>
</ha-wa-dialog>
</ha-dialog>
`;
}
static styles: CSSResultGroup = [
haStyleDialog,
css`
ha-wa-dialog {
ha-dialog {
--dialog-content-padding: 0;
}
`,
@@ -9,8 +9,8 @@ import { copyToClipboard } from "../../../common/util/copy-clipboard";
import { subscribePollingCollection } from "../../../common/util/subscribe-polling";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-card";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-metric";
import "../../../components/ha-spinner";
import type { HassioStats } from "../../../data/hassio/common";
@@ -62,24 +62,20 @@ class DialogSystemInformation extends LitElement {
@state() private _coreStats?: HassioStats;
@state() private _open = false;
@state() private _opened = false;
private _systemHealthSubscription?: Promise<UnsubscribeFunc>;
private _hassIOSubscription?: UnsubscribeFunc;
public showDialog(): void {
this._open = true;
this._opened = true;
this.hass!.loadBackendTranslation("system_health");
this._subscribe();
}
public closeDialog() {
this._open = false;
}
private _dialogClosed(): void {
this._open = false;
this._opened = false;
this._unsubscribe();
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -130,20 +126,20 @@ class DialogSystemInformation extends LitElement {
}
protected render() {
if (!this._open) {
if (!this._opened) {
return nothing;
}
const sections = this._getSections();
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize(
"ui.panel.config.repairs.system_information"
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.repairs.system_information")
)}
@closed=${this._dialogClosed}
>
<div>
${this._resolutionInfo
@@ -228,12 +224,10 @@ class DialogSystemInformation extends LitElement {
</div>
`}
</div>
<ha-dialog-footer slot="footer">
<ha-button slot="primaryAction" @click=${this._copyInfo}>
${this.hass.localize("ui.panel.config.repairs.copy")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
<ha-button slot="primaryAction" @click=${this._copyInfo}>
${this.hass.localize("ui.panel.config.repairs.copy")}
</ha-button>
</ha-dialog>
`;
}
@@ -2,7 +2,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { relativeTime } from "../../../common/datetime/relative_time";
import { capitalizeFirstLetter } from "../../../common/string/capitalize-first-letter";
import { STRINGS_SEPARATOR_DOT } from "../../../common/const";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import { domainToName } from "../../../data/integration";
@@ -100,7 +99,7 @@ class HaConfigRepairs extends LitElement {
${(issue.severity === "critical" ||
issue.severity === "error") &&
issue.created
? STRINGS_SEPARATOR_DOT
? " · "
: ""}
${createdBy
? html`<span .title=${createdBy}>${createdBy}</span>`
@@ -52,6 +52,7 @@ import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
@@ -54,6 +54,7 @@ import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
@@ -571,6 +571,7 @@ export class HaManualScriptEditor extends SubscribeMixin(LitElement) {
}
private _saveScript() {
this.triggerCloseSidebar();
fireEvent(this, "save-script");
}
@@ -55,16 +55,17 @@ export class StorageBreakdownChart extends LitElement {
<span class="heading">${heading}</span>
<span class="description">${description}</span>
</div>
<ha-icon-button
.path=${this._chartType === "sunburst"
? mdiViewArray
: mdiChartDonutVariant}
.label=${this.hass.localize(
"ui.panel.config.storage.change_chart_type"
)}
.disabled=${!hasChildren}
@click=${this._handleChartTypeChange}
></ha-icon-button>
${hasChildren
? html`<ha-icon-button
.path=${this._chartType === "sunburst"
? mdiViewArray
: mdiChartDonutVariant}
.label=${this.hass.localize(
"ui.panel.config.storage.change_chart_type"
)}
@click=${this._handleChartTypeChange}
></ha-icon-button>`
: nothing}
</div>
<div class="chart-container ${this._chartType}">
@@ -105,11 +106,9 @@ export class StorageBreakdownChart extends LitElement {
storageInfo: HostDisksUsage | null | undefined
) => {
let totalSpaceGB = hostInfo.disk_total;
let usedSpaceGB = hostInfo.disk_used;
let freeSpaceGB =
hostInfo.disk_free || hostInfo.disk_total - hostInfo.disk_used;
// hostInfo.disk_used doesn't include system reserved space,
// so we calculate used space based on total and free space
let usedSpaceGB = totalSpaceGB - freeSpaceGB;
if (storageInfo) {
const totalSpace =
@@ -214,24 +213,26 @@ export class StorageBreakdownChart extends LitElement {
static styles = css`
.header {
display: flex;
align-items: flex-end;
gap: var(--ha-space-2);
align-items: flex-start;
justify-content: space-between;
margin-bottom: var(--ha-space-2);
}
.heading-text {
display: flex;
flex: 1;
flex-direction: column;
gap: var(--ha-space-1);
}
.heading {
font-weight: 500;
font-size: var(--ha-font-size-m);
color: var(--primary-text-color);
line-height: var(--ha-line-height-expanded);
margin-right: var(--ha-space-2);
}
.description {
font-size: var(--ha-font-size-s);
color: var(--secondary-text-color);
line-height: var(--ha-line-height-expanded);
}
ha-icon-button {
+43 -33
View File
@@ -1,16 +1,17 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-spinner";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-icon-button";
import "../../../components/ha-settings-row";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import "../../../components/ha-wa-dialog";
import { createAuthForUser } from "../../../data/auth";
import type { User } from "../../../data/user";
import {
@@ -35,6 +36,8 @@ export class DialogAddUser extends LitElement {
@state() private _params?: AddUserDialogParams;
@state() private _open = false;
@state() private _name?: string;
@state() private _username?: string;
@@ -66,6 +69,8 @@ export class DialogAddUser extends LitElement {
} else {
this._allowChangeName = true;
}
this._open = true;
}
protected firstUpdated(changedProperties: PropertyValues) {
@@ -83,15 +88,14 @@ export class DialogAddUser extends LitElement {
}
return html`
<ha-dialog
open
@closed=${this._close}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.users.add_user.caption")
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize(
"ui.panel.config.users.add_user.caption"
)}
width="medium"
@closed=${this._dialogClosed}
>
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
@@ -109,7 +113,7 @@ export class DialogAddUser extends LitElement {
)}
@input=${this._handleValueChanged}
@blur=${this._maybePopulateUsername}
dialogInitialFocus
autofocus
></ha-textfield>`
: ""}
<ha-textfield
@@ -122,7 +126,7 @@ export class DialogAddUser extends LitElement {
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize("ui.common.error_required")}
dialogInitialFocus
?autofocus=${!this._allowChangeName}
></ha-textfield>
<ha-password-field
@@ -191,30 +195,37 @@ export class DialogAddUser extends LitElement {
: nothing}
</div>
<ha-button
slot="primaryAction"
appearance="plain"
@click=${this._close}
>
${this.hass!.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
.disabled=${!this._name ||
!this._username ||
!this._password ||
this._password !== this._passwordConfirm}
@click=${this._createUser}
.loading=${this._loading}
>
${this.hass.localize("ui.panel.config.users.add_user.create")}
</ha-button>
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this._close}
>
${this.hass!.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
.disabled=${!this._name ||
!this._username ||
!this._password ||
this._password !== this._passwordConfirm}
@click=${this._createUser}
.loading=${this._loading}
>
${this.hass.localize("ui.panel.config.users.add_user.create")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
private _close() {
this._open = false;
}
private _dialogClosed(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _maybePopulateUsername() {
@@ -297,8 +308,7 @@ export class DialogAddUser extends LitElement {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
ha-wa-dialog {
--dialog-z-index: 10;
}
.row {
@@ -3,11 +3,12 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../components/ha-form/types";
import "../../../components/ha-textfield";
import "../../../components/ha-button";
import "../../../components/ha-wa-dialog";
import { adminChangePassword } from "../../../data/auth";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@@ -48,6 +49,8 @@ class DialogAdminChangePassword extends LitElement {
@state() private _params?: AdminChangePasswordDialogParams;
@state() private _open = false;
@state() private _userId?: string;
@state() private _data?: FormData;
@@ -61,13 +64,24 @@ class DialogAdminChangePassword extends LitElement {
public showDialog(params: AdminChangePasswordDialogParams): void {
this._params = params;
this._userId = params.userId;
this._data = undefined;
this._error = undefined;
this._submitting = false;
this._success = false;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._params = undefined;
this._userId = undefined;
this._data = undefined;
this._submitting = false;
this._success = false;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -104,15 +118,13 @@ class DialogAdminChangePassword extends LitElement {
);
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.users.change_password.caption")
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize(
"ui.panel.config.users.change_password.caption"
)}
@closed=${this._dialogClosed}
>
${this._success
? html`
@@ -121,12 +133,15 @@ class DialogAdminChangePassword extends LitElement {
"ui.panel.config.users.change_password.password_changed"
)}
</p>
<ha-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.ok")}
</ha-button>
<ha-dialog-footer slot="footer">
<ha-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.ok")}
</ha-button>
</ha-dialog-footer>
`
: html`
<ha-form
autofocus
.hass=${this.hass}
.data=${this._data}
.error=${this._error}
@@ -136,24 +151,26 @@ class DialogAdminChangePassword extends LitElement {
@value-changed=${this._valueChanged}
.disabled=${this._submitting}
></ha-form>
<ha-button
appearance="plain"
slot="primaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._changePassword}
.disabled=${this._submitting || !canSubmit}
>
${this.hass.localize(
"ui.panel.config.users.change_password.change"
)}
</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._changePassword}
.disabled=${this._submitting || !canSubmit}
>
${this.hass.localize(
"ui.panel.config.users.change_password.change"
)}
</ha-button>
</ha-dialog-footer>
`}
</ha-dialog>
</ha-wa-dialog>
`;
}
+49 -38
View File
@@ -2,15 +2,17 @@ import { mdiPencil } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
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-icon-button";
import "../../../components/ha-label";
import "../../../components/ha-settings-row";
import "../../../components/ha-svg-icon";
import "../../../components/ha-switch";
import "../../../components/ha-textfield";
import "../../../components/ha-wa-dialog";
import { adminChangeUsername } from "../../../data/auth";
import {
computeUserBadges,
@@ -42,6 +44,8 @@ class DialogUserDetail extends LitElement {
@state() private _params?: UserDetailDialogParams;
@state() private _open = false;
@state() private _submitting = false;
public async showDialog(params: UserDetailDialogParams): Promise<void> {
@@ -51,6 +55,7 @@ class DialogUserDetail extends LitElement {
this._isAdmin = params.entry.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = params.entry.local_only;
this._isActive = params.entry.is_active;
this._open = true;
await this.updateComplete;
}
@@ -61,12 +66,12 @@ class DialogUserDetail extends LitElement {
const user = this._params.entry;
const badges = computeUserBadges(this.hass, user, true);
return html`
<ha-dialog
open
@closed=${this._close}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(this.hass, user.name)}
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${user.name}
width="medium"
@closed=${this._dialogClosed}
>
<div>
${this._error
@@ -94,7 +99,7 @@ class DialogUserDetail extends LitElement {
${!user.system_generated
? html`
<ha-textfield
dialogInitialFocus
autofocus
.value=${this._name}
@input=${this._nameChanged}
.label=${this.hass!.localize(
@@ -219,32 +224,36 @@ class DialogUserDetail extends LitElement {
: nothing}
</div>
<ha-button
slot="secondaryAction"
variant="danger"
appearance="plain"
@click=${this._deleteEntry}
.disabled=${this._submitting ||
user.system_generated ||
user.is_owner}
>
${this.hass!.localize("ui.panel.config.users.editor.delete_user")}
</ha-button>
<ha-button
slot="primaryAction"
appearance="plain"
@click=${this._close}
>
${this.hass!.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${!this._name || this._submitting || user.system_generated}
>
${this.hass!.localize("ui.common.save")}
</ha-button>
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
variant="danger"
appearance="plain"
@click=${this._deleteEntry}
.disabled=${this._submitting ||
user.system_generated ||
user.is_owner}
>
${this.hass!.localize("ui.panel.config.users.editor.delete_user")}
</ha-button>
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this._close}
>
${this.hass!.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${!this._name ||
this._submitting ||
user.system_generated}
>
${this.hass!.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -288,7 +297,7 @@ class DialogUserDetail extends LitElement {
this._submitting = true;
try {
if (await this._params!.removeEntry()) {
this._params = undefined;
this._close();
}
} finally {
this._submitting = false;
@@ -360,16 +369,18 @@ class DialogUserDetail extends LitElement {
}
private _close(): void {
this._open = false;
}
private _dialogClosed(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
.form {
padding-top: 16px;
}
@@ -454,7 +454,7 @@ export class AssistPref extends LitElement {
align-items: center;
padding-bottom: 0;
}
voice-assistant-brand-icon {
img {
height: 28px;
margin-right: 16px;
margin-inline-end: 16px;
@@ -280,7 +280,7 @@ export class CloudAlexaPref extends LitElement {
display: flex;
align-items: center;
}
voice-assistant-brand-icon {
img {
height: 28px;
margin-right: 16px;
margin-inline-end: 16px;
@@ -152,12 +152,11 @@ export class CloudDiscover 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);
@@ -350,7 +350,7 @@ export class CloudGooglePref extends LitElement {
display: flex;
align-items: center;
}
voice-assistant-brand-icon {
img {
height: 28px;
margin-right: 16px;
margin-inline-end: 16px;
@@ -26,29 +26,25 @@ export function getAssistantsTableColumn<T>(
valueColumn: "assistants_sortable_key",
template: (entry: any) =>
html`${entry.assistants.length !== 0
? html`<div style="display: flex; gap: var(--ha-space-4);">
${availableAssistants.map((vaId) => {
const supported =
!supportedEntities?.[vaId] ||
supportedEntities[vaId].includes(entry.entity_id);
const manual = entry.manAssistants?.includes(vaId);
return getAssistantsTableColumnIcon(
entry.entity_id,
entry.assistants.includes(vaId),
vaId,
hass,
entitiesToCheck,
manual,
!supported
);
})}
</div>`
? availableAssistants.map((vaId) => {
const supported =
!supportedEntities?.[vaId] ||
supportedEntities[vaId].includes(entry.entity_id);
const manual = entry.manAssistants?.includes(vaId);
return getAssistantsTableColumnIcon(
entry.assistants.includes(vaId),
vaId,
hass,
entitiesToCheck,
manual,
!supported
);
})
: nothing}`,
};
}
export const getAssistantsTableColumnIcon = (
id: string,
show: boolean,
vaId: string,
hass: HomeAssistant,
@@ -61,7 +57,6 @@ export const getAssistantsTableColumnIcon = (
);
return show
? html`<voice-assistants-expose-assistant-icon
.id=${id}
.assistant=${vaId}
.hass=${hass}
.manual=${manual ?? false}
@@ -2,7 +2,6 @@ import { mdiAlertCircle } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { slugify } from "../../../../common/string/slugify";
import { voiceAssistants } from "../../../../data/expose";
import type { HomeAssistant } from "../../../../types";
import "../../../../components/ha-svg-icon";
@@ -24,9 +23,8 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
render() {
if (!this.assistant || !voiceAssistants[this.assistant]) return nothing;
const id = slugify(this.id) + "-" + this.assistant;
return html`
<div class="container" id=${id}>
<div class="container" id="container">
<voice-assistant-brand-icon
style=${styleMap({
filter: this.manual ? "grayscale(100%)" : undefined,
@@ -45,7 +43,7 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
: nothing}
</div>
<ha-tooltip
for=${id}
for="container"
placement="left"
.disabled=${!this.unsupported && !this.manual}
>
@@ -68,6 +66,13 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
.container {
position: relative;
}
.logo {
position: relative;
height: 24px;
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
}
.unsupported {
color: var(--error-color);
position: absolute;
@@ -1,9 +1,8 @@
import type { HassConfig } from "home-assistant-js-websocket";
import {
differenceInMonths,
subHours,
differenceInDays,
differenceInMonths,
differenceInCalendarMonths,
differenceInYears,
startOfYear,
addMilliseconds,
@@ -13,7 +12,6 @@ import {
addHours,
startOfDay,
addDays,
subDays,
} from "date-fns";
import type {
BarSeriesOption,
@@ -35,22 +33,10 @@ import { filterXSS } from "../../../../../common/util/xss";
import type { StatisticPeriod } from "../../../../../data/recorder";
import { getSuggestedPeriod } from "../../../../../data/energy";
// Number of days of padding when showing time axis in months
const MONTH_TIME_AXIS_PADDING = 5;
export function getSuggestedMax(
period: StatisticPeriod,
end: Date,
noRounding: boolean
): Date {
// Maximum period depends on whether plotting a line chart or discrete bars.
// - For line charts we must be plotting all the way to end of a given period,
// otherwise we cut off the last period of data.
// - For bar charts we need to round down to the start of the final bars period
// to avoid unnecessary padding of the chart.
export function getSuggestedMax(period: StatisticPeriod, end: Date): Date {
let suggestedMax = new Date(end);
if (noRounding || period === "5minute") {
if (period === "5minute") {
return suggestedMax;
}
suggestedMax.setMinutes(0, 0, 0);
@@ -96,44 +82,17 @@ export function getCommonOptions(
detailedDailyData = false
): ECOption {
const suggestedPeriod = getSuggestedPeriod(start, end, detailedDailyData);
const suggestedMax = getSuggestedMax(suggestedPeriod, end, detailedDailyData);
const compare = compareStart !== undefined && compareEnd !== undefined;
const showCompareYear =
compare && start.getFullYear() !== compareStart.getFullYear();
const monthTimeAxis: ECOption = {
xAxis: {
type: "time",
min: subDays(start, MONTH_TIME_AXIS_PADDING),
max: addDays(suggestedMax, MONTH_TIME_AXIS_PADDING),
axisLabel: {
formatter: {
year: "{yearStyle|{MMMM} {yyyy}}",
month: "{MMMM}",
},
rich: {
yearStyle: {
fontWeight: "bold",
},
},
},
// For shorter month ranges, force splitting to ensure time axis renders
// as whole month intervals. Limit the number of forced ticks to 6 months
// (so a max calendar difference of 5) to reduce clutter.
splitNumber: Math.min(differenceInCalendarMonths(end, start), 5),
},
};
const normalTimeAxis: ECOption = {
const options: ECOption = {
xAxis: {
type: "time",
min: start,
max: suggestedMax,
max: getSuggestedMax(suggestedPeriod, end),
},
};
const options: ECOption = {
...(suggestedPeriod === "month" ? monthTimeAxis : normalTimeAxis),
yAxis: {
type: "value",
name: unit,
+2 -2
View File
@@ -13,7 +13,7 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { BINARY_STATE_ON, STRINGS_SEPARATOR_DOT } from "../../../common/const";
import { BINARY_STATE_ON } from "../../../common/const";
import { computeAreaName } from "../../../common/entity/compute_area_name";
import { generateEntityFilter } from "../../../common/entity/entity_filter";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
@@ -522,7 +522,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
return `${formattedValue}${formattedUnit}`;
})
.filter(Boolean)
.join(STRINGS_SEPARATOR_DOT);
.join(" · ");
return sensorStates;
}
@@ -7,9 +7,7 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { getGraphColorByIndex } from "../../../common/color/colors";
import { computeCssColor } from "../../../common/color/compute-color";
import { computeDomain } from "../../../common/entity/compute_domain";
import { normalizeValueBySIPrefix } from "../../../common/number/normalize-by-si-prefix";
import { MobileAwareMixin } from "../../../mixins/mobile-aware-mixin";
import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
@@ -35,7 +33,6 @@ const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
interface ProcessedEntity {
entity: string;
name?: string | EntityNameItem | EntityNameItem[];
color?: string;
}
interface LegendItem {
@@ -150,7 +147,6 @@ export class HuiDistributionCard
this._configEntities = entities.map((entity) => ({
entity: entity.entity,
name: entity.name,
color: entity.color,
}));
}
@@ -231,16 +227,10 @@ export class HuiDistributionCard
const stateObj = this.hass!.states[entity.entity];
if (!stateObj) return;
const rawValue = Number(stateObj.state);
if (rawValue <= 0 || isNaN(rawValue)) return;
const value = normalizeValueBySIPrefix(
rawValue,
stateObj.attributes.unit_of_measurement
);
const value = Number(stateObj.state);
if (value <= 0 || isNaN(value)) return;
const color = entity.color
? computeCssColor(entity.color)
: getGraphColorByIndex(entity.originalIndex, computedStyles);
const color = getGraphColorByIndex(entity.originalIndex, computedStyles);
const name = computeLovelaceEntityName(this.hass!, stateObj, entity.name);
const formattedValue = this.hass!.formatEntityState(stateObj);
@@ -289,9 +279,7 @@ export class HuiDistributionCard
name: name,
value: value,
formattedValue: formattedValue,
color: entity.color
? computeCssColor(entity.color)
: getGraphColorByIndex(index, computedStyles),
color: getGraphColorByIndex(index, computedStyles),
isHidden: isHidden,
isDisabled: isZeroOrNegative,
};
+19 -5
View File
@@ -13,6 +13,11 @@ import {
stateColorCss,
} from "../../../common/entity/state_color";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import {
formatNumber,
getNumberFormatOptions,
isNumericState,
} from "../../../common/number/format_number";
import { iconColorCSS } from "../../../common/style/icon_color_css";
import "../../../components/ha-attribute-value";
import "../../../components/ha-card";
@@ -120,7 +125,6 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
}
const domain = computeStateDomain(stateObj);
const stateParts = this.hass.formatEntityStateToParts(stateObj);
let unit;
if (
@@ -130,9 +134,9 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
) {
unit = this._config.unit;
if (!unit) {
if (!this._config.attribute) {
unit = stateParts.find((part) => part.type === "unit")?.value;
} else {
if (!this._config.attribute)
unit = stateObj.attributes.unit_of_measurement;
else {
const parts = this.hass.formatEntityAttributeValueToParts(
stateObj,
this._config.attribute
@@ -201,7 +205,17 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
>
</ha-attribute-value>`
: this.hass.localize("state.default.unknown")
: stateParts.find((part) => part.type === "value")?.value}</span
: (isNumericState(stateObj) || this._config.unit) &&
stateObj.attributes.device_class !== "duration"
? formatNumber(
stateObj.state,
this.hass.locale,
getNumberFormatOptions(
stateObj,
this.hass.entities[this._config.entity]
)
)
: this.hass.formatEntityState(stateObj)}</span
>
${unit ? html`<span class="measurement">${unit}</span>` : nothing}
</div>
@@ -332,11 +332,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
.maxYAxis=${this._config.max_y_axis}
.startTime=${this._energyStart}
.endTime=${this._energyEnd && this._energyStart
? getSuggestedMax(
this._period!,
this._energyEnd,
(this._config.chart_type ?? "line") === "line"
)
? getSuggestedMax(this._period!, this._energyEnd)
: undefined}
.fitYData=${this._config.fit_y_data || false}
.hideLegend=${this._config.hide_legend || false}
+1 -4
View File
@@ -94,7 +94,6 @@ export interface EntitiesCardEntityConfig extends EntityConfig {
| "last-changed"
| "last-triggered"
| "last-updated"
| "area"
| "position"
| "state"
| "tilt-position"
@@ -665,9 +664,7 @@ export interface HomeSummaryCard extends LovelaceCardConfig {
double_tap_action?: ActionConfig;
}
export interface DistributionEntityConfig extends EntityConfig {
color?: string;
}
export interface DistributionEntityConfig extends EntityConfig {}
export interface DistributionCardConfig extends LovelaceCardConfig {
type: "distribution";

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