mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-15 19:57:41 +00:00
Compare commits
35 Commits
clock-date
...
animate-ha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c66b83d5f6 | ||
|
|
1d241aa49a | ||
|
|
fece231faf | ||
|
|
fffb3c3a28 | ||
|
|
fe14d436ff | ||
|
|
42e02be928 | ||
|
|
6213b6cd2a | ||
|
|
37e35a3026 | ||
|
|
cd75c55392 | ||
|
|
ca325020d7 | ||
|
|
6250402661 | ||
|
|
0bfca79851 | ||
|
|
49bddf6139 | ||
|
|
0daf94e98f | ||
|
|
00a3237611 | ||
|
|
53deb3f419 | ||
|
|
6c1c7cead3 | ||
|
|
f8d65cc0ec | ||
|
|
5be7bad176 | ||
|
|
0a54a93a39 | ||
|
|
156583aff1 | ||
|
|
7572257821 | ||
|
|
4703cf802f | ||
|
|
55c2315329 | ||
|
|
7d7e95ac55 | ||
|
|
6d7694caff | ||
|
|
d7b6243698 | ||
|
|
73feef9e92 | ||
|
|
453a546574 | ||
|
|
52c0e6f1f5 | ||
|
|
444f8d87b3 | ||
|
|
57a586c3a7 | ||
|
|
1975265e6b | ||
|
|
66e6cb8dbc | ||
|
|
9ce9d254f8 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,7 +15,7 @@ dist/
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
/node_modules/
|
||||
node_modules/
|
||||
yarn-error.log
|
||||
npm-debug.log
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockAreaRegistry = (
|
||||
|
||||
@@ -10,7 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
|
||||
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"@vitest/coverage-v8": "4.0.17",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
@@ -217,7 +217,7 @@
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.52.0",
|
||||
"vite-tsconfig-paths": "6.0.4",
|
||||
"vitest": "4.0.16",
|
||||
"vitest": "4.0.17",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
@@ -236,6 +236,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.12.0"
|
||||
"node": "24.13.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
|
||||
export interface AreasFloorHierarchy {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
|
||||
export const computeAreaName = (area: AreaRegistryEntry): string | undefined =>
|
||||
area.name?.trim();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
// From https://github.com/epoberezkin/fast-deep-equal
|
||||
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
|
||||
export const deepEqual = (a: any, b: any): boolean => {
|
||||
|
||||
interface DeepEqualOptions {
|
||||
/** Compare Symbol properties in addition to string keys */
|
||||
compareSymbols?: boolean;
|
||||
}
|
||||
|
||||
export const deepEqual = (
|
||||
a: any,
|
||||
b: any,
|
||||
options?: DeepEqualOptions
|
||||
): boolean => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
@@ -18,7 +28,7 @@ export const deepEqual = (a: any, b: any): boolean => {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
if (!deepEqual(a[i], b[i], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +45,7 @@ export const deepEqual = (a: any, b: any): boolean => {
|
||||
}
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!deepEqual(i[1], b.get(i[0]))) {
|
||||
if (!deepEqual(i[1], b.get(i[0]), options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,11 +103,28 @@ export const deepEqual = (a: any, b: any): boolean => {
|
||||
for (i = length; i-- !== 0; ) {
|
||||
const key = keys[i];
|
||||
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
if (!deepEqual(a[key], b[key], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Compare Symbol properties if requested
|
||||
if (options?.compareSymbols) {
|
||||
const symbolsA = Object.getOwnPropertySymbols(a);
|
||||
const symbolsB = Object.getOwnPropertySymbols(b);
|
||||
if (symbolsA.length !== symbolsB.length) {
|
||||
return false;
|
||||
}
|
||||
for (const sym of symbolsA) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, sym)) {
|
||||
return false;
|
||||
}
|
||||
if (!deepEqual(a[sym], b[sym], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,13 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { LitElement, html, css } from "lit";
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
|
||||
import type { CallbackDataParams } from "echarts/types/src/util/types";
|
||||
import type {
|
||||
CallbackDataParams,
|
||||
ECElementEvent,
|
||||
} from "echarts/types/src/util/types";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import SankeyChart from "../../resources/echarts/components/sankey/install";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
@@ -21,6 +25,7 @@ export interface Node {
|
||||
label?: string;
|
||||
color?: string;
|
||||
passThrough?: boolean;
|
||||
entityId?: string;
|
||||
}
|
||||
export interface Link {
|
||||
source: string;
|
||||
@@ -83,6 +88,7 @@ export class HaSankeyChart extends LitElement {
|
||||
.options=${options}
|
||||
height="100%"
|
||||
.extraComponents=${[SankeyChart]}
|
||||
@chart-click=${this._handleChartClick}
|
||||
></ha-chart-base>`;
|
||||
}
|
||||
|
||||
@@ -103,6 +109,22 @@ export class HaSankeyChart extends LitElement {
|
||||
return null;
|
||||
};
|
||||
|
||||
private _handleChartClick = (ev: CustomEvent<ECElementEvent>) => {
|
||||
const detail = ev.detail;
|
||||
// Only handle node clicks (not links)
|
||||
if (detail.dataType !== "node") {
|
||||
return;
|
||||
}
|
||||
const nodeId = (detail.data as Record<string, any>)?.id;
|
||||
if (!nodeId) {
|
||||
return;
|
||||
}
|
||||
const node = this.data.nodes.find((n) => n.id === nodeId);
|
||||
if (node?.entityId) {
|
||||
fireEvent(this, "node-click", { node });
|
||||
}
|
||||
};
|
||||
|
||||
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
|
||||
const filteredNodes = data.nodes.filter((n) => n.value > 0);
|
||||
const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();
|
||||
@@ -294,4 +316,7 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-sankey-chart": HaSankeyChart;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"node-click": { node: Node };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { expose } from "comlink";
|
||||
import Fuse, { type FuseOptionKey } from "fuse.js";
|
||||
import type { FuseOptionKey, IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ipCompare, stringCompare } from "../../common/string/compare";
|
||||
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
||||
import { multiTermSearch } from "../../resources/fuseMultiTerm";
|
||||
import type {
|
||||
ClonedDataTableColumnData,
|
||||
DataTableRowData,
|
||||
@@ -11,46 +11,159 @@ import type {
|
||||
SortingDirection,
|
||||
} from "./ha-data-table";
|
||||
|
||||
const getSearchKeys = memoizeOne(
|
||||
(columns: SortableColumnContainer): FuseOptionKey<DataTableRowData>[] => {
|
||||
const searchKeys = new Set<string>();
|
||||
interface FilterKeyConfig {
|
||||
key: string;
|
||||
filterKey?: string;
|
||||
}
|
||||
|
||||
Object.entries(columns).forEach(([key, column]) => {
|
||||
if (column.filterable) {
|
||||
searchKeys.add(
|
||||
column.filterKey
|
||||
? `${column.valueColumn || key}.${column.filterKey}`
|
||||
: key
|
||||
);
|
||||
}
|
||||
});
|
||||
return Array.from(searchKeys);
|
||||
const getFilterKeys = memoizeOne(
|
||||
(columns: SortableColumnContainer): FilterKeyConfig[] =>
|
||||
Object.entries(columns)
|
||||
.filter(([, column]) => column.filterable)
|
||||
.map(([key, column]) => ({
|
||||
key: column.valueColumn || key,
|
||||
filterKey: column.filterKey,
|
||||
}))
|
||||
);
|
||||
|
||||
const getSearchableValue = (
|
||||
row: DataTableRowData,
|
||||
{ key, filterKey }: FilterKeyConfig
|
||||
): string => {
|
||||
let value = row[key];
|
||||
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
);
|
||||
|
||||
const fuseIndex = memoizeOne(
|
||||
(data: DataTableRowData[], keys: FuseOptionKey<DataTableRowData>[]) =>
|
||||
Fuse.createIndex(keys, data)
|
||||
);
|
||||
if (filterKey && typeof value === "object" && !Array.isArray(value)) {
|
||||
value = value[filterKey];
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const stringValues = value
|
||||
.filter((item) => item != null && typeof item !== "object")
|
||||
.map(String);
|
||||
return stripDiacritics(stringValues.join(" ").toLowerCase());
|
||||
}
|
||||
|
||||
return stripDiacritics(String(value).toLowerCase());
|
||||
};
|
||||
|
||||
/** Filters data using exact substring matching (all terms must match). */
|
||||
const filterDataExact = (
|
||||
data: DataTableRowData[],
|
||||
filterKeys: FilterKeyConfig[],
|
||||
terms: string[]
|
||||
): DataTableRowData[] => {
|
||||
if (terms.length === 1) {
|
||||
const term = terms[0];
|
||||
return data.filter((row) =>
|
||||
filterKeys.some((config) =>
|
||||
getSearchableValue(row, config).includes(term)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return data.filter((row) => {
|
||||
const searchString = filterKeys
|
||||
.map((config) => getSearchableValue(row, config))
|
||||
.join(" ");
|
||||
return terms.every((term) => searchString.includes(term));
|
||||
});
|
||||
};
|
||||
|
||||
const FUZZY_OPTIONS: IFuseOptions<DataTableRowData> = {
|
||||
ignoreDiacritics: true,
|
||||
isCaseSensitive: false,
|
||||
threshold: 0.2, // Stricter than default 0.3
|
||||
minMatchCharLength: 2,
|
||||
ignoreLocation: true,
|
||||
shouldSort: false,
|
||||
};
|
||||
|
||||
interface FuseKeyConfig {
|
||||
name: string | string[];
|
||||
getFn: (row: DataTableRowData) => string;
|
||||
}
|
||||
|
||||
/** Filters data using fuzzy matching with Fuse.js (all terms must match). */
|
||||
const filterDataFuzzy = (
|
||||
data: DataTableRowData[],
|
||||
filterKeys: FilterKeyConfig[],
|
||||
terms: string[]
|
||||
): DataTableRowData[] => {
|
||||
// Build Fuse.js search keys from filter keys
|
||||
const fuseKeys: FuseKeyConfig[] = filterKeys.map((config) => ({
|
||||
name: config.filterKey ? [config.key, config.filterKey] : config.key,
|
||||
getFn: (row: DataTableRowData) => getSearchableValue(row, config),
|
||||
}));
|
||||
|
||||
// Find minimum term length to adjust minMatchCharLength
|
||||
const minTermLength = Math.min(...terms.map((t) => t.length));
|
||||
const minMatchCharLength = Math.min(minTermLength, 2);
|
||||
|
||||
const fuse = new Fuse<DataTableRowData>(data, {
|
||||
...FUZZY_OPTIONS,
|
||||
keys: fuseKeys as FuseOptionKey<DataTableRowData>[],
|
||||
minMatchCharLength,
|
||||
});
|
||||
|
||||
// For single term, simple search
|
||||
if (terms.length === 1) {
|
||||
return fuse.search(terms[0]).map((r) => r.item);
|
||||
}
|
||||
|
||||
// For multiple terms, all must match (AND logic)
|
||||
const expression = {
|
||||
$and: terms.map((term) => ({
|
||||
$or: fuseKeys.map((key) => ({
|
||||
$path: Array.isArray(key.name) ? key.name : [key.name],
|
||||
$val: term,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
return fuse.search(expression).map((r) => r.item);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters data with exact match priority and fuzzy fallback.
|
||||
* - First tries exact substring matching
|
||||
* - If exact matches found, returns only those
|
||||
* - If no exact matches, falls back to fuzzy search with strict scoring
|
||||
*/
|
||||
const filterData = (
|
||||
data: DataTableRowData[],
|
||||
columns: SortableColumnContainer,
|
||||
filter: string
|
||||
) => {
|
||||
filter = stripDiacritics(filter.toLowerCase());
|
||||
): DataTableRowData[] => {
|
||||
const normalizedFilter = stripDiacritics(filter.toLowerCase().trim());
|
||||
|
||||
if (filter === "") {
|
||||
if (!normalizedFilter) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const keys = getSearchKeys(columns);
|
||||
const filterKeys = getFilterKeys(columns);
|
||||
|
||||
const index = fuseIndex(data, keys);
|
||||
if (!filterKeys.length) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return multiTermSearch<DataTableRowData>(data, filter, keys, index, {
|
||||
threshold: 0.2, // reduce fuzzy matches in data tables
|
||||
});
|
||||
const terms = normalizedFilter.split(/\s+/);
|
||||
|
||||
// First, try exact substring matching
|
||||
const exactMatches = filterDataExact(data, filterKeys, terms);
|
||||
|
||||
if (exactMatches.length > 0) {
|
||||
return exactMatches;
|
||||
}
|
||||
|
||||
// No exact matches, fall back to fuzzy search
|
||||
return filterDataFuzzy(data, filterKeys, terms);
|
||||
};
|
||||
|
||||
const sortData = (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import type { DeviceAutomation } from "../../data/device/device_automation";
|
||||
import {
|
||||
@@ -11,11 +12,12 @@ import {
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-md-select";
|
||||
import "../ha-md-select-option";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
|
||||
|
||||
export abstract class HaDeviceAutomationPicker<
|
||||
T extends DeviceAutomation,
|
||||
@@ -28,7 +30,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
|
||||
@property({ type: Object }) public value?: T;
|
||||
|
||||
@state() private _automations: T[] = [];
|
||||
@state() private _automations?: T[];
|
||||
|
||||
// Trigger an empty render so we start with a clean DOM.
|
||||
// paper-listbox does not like changing things around.
|
||||
@@ -44,12 +46,6 @@ export abstract class HaDeviceAutomationPicker<
|
||||
);
|
||||
}
|
||||
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
);
|
||||
}
|
||||
|
||||
private _localizeDeviceAutomation: (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
@@ -75,7 +71,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
if (!this.value) {
|
||||
if (!this.value || !this._automations) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -88,7 +84,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
);
|
||||
|
||||
if (idx === -1) {
|
||||
return UNKNOWN_AUTOMATION_KEY;
|
||||
return this.value.alias || this.value.type || "unknown";
|
||||
}
|
||||
|
||||
return `${this._automations[idx].device_id}_${idx}`;
|
||||
@@ -99,37 +95,21 @@ export abstract class HaDeviceAutomationPicker<
|
||||
return nothing;
|
||||
}
|
||||
const value = this._value;
|
||||
return html`
|
||||
<ha-md-select
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
@change=${this._automationChanged}
|
||||
@closed=${stopPropagation}
|
||||
.disabled=${this._automations.length === 0}
|
||||
>
|
||||
${value === NO_AUTOMATION_KEY
|
||||
? html`<ha-md-select-option .value=${NO_AUTOMATION_KEY}>
|
||||
${this.NO_AUTOMATION_TEXT}
|
||||
</ha-md-select-option>`
|
||||
: nothing}
|
||||
${value === UNKNOWN_AUTOMATION_KEY
|
||||
? html`<ha-md-select-option .value=${UNKNOWN_AUTOMATION_KEY}>
|
||||
${this.UNKNOWN_AUTOMATION_TEXT}
|
||||
</ha-md-select-option>`
|
||||
: nothing}
|
||||
${this._automations.map(
|
||||
(automation, idx) => html`
|
||||
<ha-md-select-option .value=${`${automation.device_id}_${idx}`}>
|
||||
${this._localizeDeviceAutomation(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
automation
|
||||
)}
|
||||
</ha-md-select-option>
|
||||
`
|
||||
)}
|
||||
</ha-md-select>
|
||||
`;
|
||||
|
||||
return html`<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
.disabled=${!this._automations || this._automations.length === 0}
|
||||
.getItems=${this._getItems(value, this._automations)}
|
||||
@value-changed=${this._automationChanged}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
)}
|
||||
hide-clear-icon
|
||||
>
|
||||
</ha-generic-picker>`;
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
@@ -140,6 +120,57 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
}
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(value: string, automations: T[] | undefined) => {
|
||||
if (!automations) {
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
const automationListItems = automations.map((automation, idx) => {
|
||||
const primary = this._localizeDeviceAutomation(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
automation
|
||||
);
|
||||
return {
|
||||
id: `${automation.device_id}_${idx}`,
|
||||
primary,
|
||||
};
|
||||
});
|
||||
|
||||
automationListItems.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(
|
||||
a.primary,
|
||||
b.primary,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
|
||||
if (value === NO_AUTOMATION_KEY) {
|
||||
automationListItems.unshift({
|
||||
id: NO_AUTOMATION_KEY,
|
||||
primary: this.NO_AUTOMATION_TEXT,
|
||||
});
|
||||
}
|
||||
|
||||
return () => automationListItems;
|
||||
}
|
||||
);
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||
const automation = this._automations?.find(
|
||||
(a, idx) => value === `${a.device_id}_${idx}`
|
||||
);
|
||||
|
||||
const text = automation
|
||||
? this._localizeDeviceAutomation(this.hass, this._entityReg, automation)
|
||||
: value === NO_AUTOMATION_KEY
|
||||
? this.NO_AUTOMATION_TEXT
|
||||
: value;
|
||||
|
||||
return html`<span slot="headline">${text}</span>`;
|
||||
};
|
||||
|
||||
private async _updateDeviceInfo() {
|
||||
this._automations = this.deviceId
|
||||
? (await this._fetchDeviceAutomations(this.hass, this.deviceId)).sort(
|
||||
@@ -161,13 +192,14 @@ export abstract class HaDeviceAutomationPicker<
|
||||
this._renderEmpty = false;
|
||||
}
|
||||
|
||||
private _automationChanged(ev) {
|
||||
const value = ev.target.value;
|
||||
if (!value || [UNKNOWN_AUTOMATION_KEY, NO_AUTOMATION_KEY].includes(value)) {
|
||||
private _automationChanged(ev: CustomEvent<{ value: string }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (!value || NO_AUTOMATION_KEY === value) {
|
||||
return;
|
||||
}
|
||||
const [deviceId, idx] = value.split("_");
|
||||
const automation = this._automations[idx];
|
||||
const automation = this._automations![idx];
|
||||
if (automation.device_id !== deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import {
|
||||
STATE_DISPLAY_SPECIAL_CONTENT,
|
||||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS,
|
||||
@@ -95,6 +96,9 @@ export class HaStateContentPicker extends LitElement {
|
||||
@property({ type: Boolean, attribute: "allow-name" }) public allowName =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-context" }) public allowContext =
|
||||
false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string[] | string;
|
||||
@@ -106,7 +110,12 @@ export class HaStateContentPicker extends LitElement {
|
||||
private _editIndex?: number;
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
|
||||
(
|
||||
entityId?: string,
|
||||
stateObj?: HassEntity,
|
||||
allowName?: boolean,
|
||||
allowContext?: boolean
|
||||
) => {
|
||||
const domain = entityId ? computeDomain(entityId) : undefined;
|
||||
const items: PickerComboBoxItem[] = [
|
||||
{
|
||||
@@ -149,6 +158,52 @@ export class HaStateContentPicker extends LitElement {
|
||||
"ui.components.state-content-picker.last_updated"
|
||||
),
|
||||
},
|
||||
...(allowContext && stateObj
|
||||
? (() => {
|
||||
const context = getEntityContext(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
const contextItems: PickerComboBoxItem[] = [];
|
||||
if (context.device) {
|
||||
contextItems.push({
|
||||
id: "device_name",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.device_name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.device_name"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (context.area) {
|
||||
contextItems.push({
|
||||
id: "area_name",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.area_name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.area_name"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (context.floor) {
|
||||
contextItems.push({
|
||||
id: "floor_name",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.floor_name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.floor_name"
|
||||
),
|
||||
});
|
||||
}
|
||||
return contextItems;
|
||||
})()
|
||||
: []),
|
||||
...(domain
|
||||
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
|
||||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
|
||||
@@ -300,7 +355,8 @@ export class HaStateContentPicker extends LitElement {
|
||||
const items = this._getItems(
|
||||
this.entityId,
|
||||
stateObjForItems,
|
||||
this.allowName
|
||||
this.allowName,
|
||||
this.allowContext
|
||||
);
|
||||
return items.find((item) => item.id === value)?.primary;
|
||||
}
|
||||
@@ -343,7 +399,12 @@ export class HaStateContentPicker extends LitElement {
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
const items = this._getItems(this.entityId, stateObj, this.allowName);
|
||||
const items = this._getItems(
|
||||
this.entityId,
|
||||
stateObj,
|
||||
this.allowName,
|
||||
this.allowContext
|
||||
);
|
||||
const currentValue =
|
||||
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
||||
|
||||
@@ -367,7 +428,12 @@ export class HaStateContentPicker extends LitElement {
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
const items = this._getItems(this.entityId, stateObj, this.allowName);
|
||||
const items = this._getItems(
|
||||
this.entityId,
|
||||
stateObj,
|
||||
this.allowName,
|
||||
this.allowContext
|
||||
);
|
||||
|
||||
// If the search string does not match with the id of any of the items,
|
||||
// offer to add it as a custom attribute
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
@@ -88,6 +88,9 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
@property({ type: Boolean, attribute: "block-mode-change" })
|
||||
public blockModeChange = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "without-header" })
|
||||
public withoutHeader = false;
|
||||
|
||||
@state() private _mode: DialogSheetMode = "dialog";
|
||||
|
||||
private _unsubMediaQuery?: () => void;
|
||||
@@ -118,27 +121,33 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
if (this._mode === "bottom-sheet") {
|
||||
return html`
|
||||
<ha-bottom-sheet .open=${this.open} flexcontent>
|
||||
<ha-dialog-header
|
||||
slot="header"
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-drawer="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
${!this.withoutHeader
|
||||
? html`<ha-dialog-header
|
||||
slot="header"
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-drawer="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span
|
||||
slot="title"
|
||||
class="title"
|
||||
id="ha-wa-dialog-title"
|
||||
>
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>`
|
||||
: nothing}
|
||||
<slot></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</ha-bottom-sheet>
|
||||
@@ -156,6 +165,7 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
.headerSubtitle=${this.headerSubtitle}
|
||||
.headerSubtitlePosition=${this.headerSubtitlePosition}
|
||||
flexcontent
|
||||
.withoutHeader=${this.withoutHeader}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="headerNavigationIcon">
|
||||
<ha-icon-button
|
||||
|
||||
@@ -6,16 +6,10 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import { createAreaRegistryEntry } from "../data/area_registry";
|
||||
import type {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
} from "../data/device/device_registry";
|
||||
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
|
||||
import { areaComboBoxKeys, getAreas } from "../data/area/area_picker";
|
||||
import { createAreaRegistryEntry } from "../data/area/area_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
@@ -30,12 +24,6 @@ import "./ha-svg-icon";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "search_labels.areaName", weight: 10 },
|
||||
{ name: "search_labels.aliases", weight: 8 },
|
||||
{ name: "search_labels.floorName", weight: 6 },
|
||||
{ name: "search_labels.id", weight: 3 },
|
||||
];
|
||||
@customElement("ha-area-picker")
|
||||
export class HaAreaPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -102,6 +90,8 @@ export class HaAreaPicker extends LitElement {
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private _getAreasMemoized = memoizeOne(getAreas);
|
||||
|
||||
// Recompute value renderer when the areas change
|
||||
private _computeValueRenderer = memoizeOne(
|
||||
(_haAreas: HomeAssistant["areas"]): PickerValueRenderer =>
|
||||
@@ -137,183 +127,13 @@ export class HaAreaPicker extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getAreas = memoizeOne(
|
||||
(
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
excludeAreas: this["excludeAreas"]
|
||||
): PickerComboBoxItem[] => {
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
const areas = Object.values(haAreas);
|
||||
const devices = Object.values(haDevices);
|
||||
const entities = Object.values(haEntities);
|
||||
|
||||
if (
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses ||
|
||||
deviceFilter ||
|
||||
entityFilter
|
||||
) {
|
||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) =>
|
||||
deviceFilter!(device)
|
||||
);
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter(stateObj);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter!(stateObj);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outputAreas = areas;
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
areaIds = inputDevices
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.area_id!);
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
areaIds = (areaIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.area_id)
|
||||
.map((entity) => entity.area_id!)
|
||||
);
|
||||
}
|
||||
|
||||
if (areaIds) {
|
||||
outputAreas = outputAreas.filter((area) =>
|
||||
areaIds!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeAreas) {
|
||||
outputAreas = outputAreas.filter(
|
||||
(area) => !excludeAreas!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
const items = outputAreas.map<PickerComboBoxItem>((area) => {
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
||||
const areaName = computeAreaName(area);
|
||||
return {
|
||||
id: area.area_id,
|
||||
primary: areaName || area.area_id,
|
||||
secondary: floorName,
|
||||
icon: area.icon || undefined,
|
||||
icon_path: area.icon ? undefined : mdiTextureBox,
|
||||
search_labels: {
|
||||
areaName: areaName || null,
|
||||
floorName: floorName || null,
|
||||
id: area.area_id,
|
||||
aliases: area.aliases.join(" "),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = () =>
|
||||
this._getAreas(
|
||||
this._getAreasMemoized(
|
||||
this.hass.areas,
|
||||
this.hass.floors,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this.hass.states,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
@@ -394,7 +214,7 @@ export class HaAreaPicker extends LitElement {
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.addButtonLabel=${this.addButtonLabel}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.searchKeys=${areaComboBoxKeys}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.area-picker.unknown"
|
||||
)}
|
||||
|
||||
@@ -255,6 +255,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
...this._loadedCodeMirror.tabKeyBindings,
|
||||
saveKeyBinding,
|
||||
]),
|
||||
this._loadedCodeMirror.search({ top: true }),
|
||||
this._loadedCodeMirror.langCompartment.of(this._mode),
|
||||
this._loadedCodeMirror.haTheme,
|
||||
this._loadedCodeMirror.haSyntaxHighlighting,
|
||||
|
||||
@@ -90,6 +90,16 @@ export class HaDrawer extends DrawerBase {
|
||||
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
|
||||
inset-inline-start: 0 !important;
|
||||
inset-inline-end: initial !important;
|
||||
transition-property: transform, width;
|
||||
transition-duration:
|
||||
var(--mdc-drawer-transition-duration, 0.2s),
|
||||
var(--ha-animation-base-duration);
|
||||
transition-timing-function:
|
||||
var(
|
||||
--mdc-drawer-transition-timing-function,
|
||||
cubic-bezier(0.4, 0, 0.2, 1)
|
||||
),
|
||||
ease;
|
||||
}
|
||||
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
|
||||
z-index: 200;
|
||||
@@ -103,6 +113,15 @@ export class HaDrawer extends DrawerBase {
|
||||
direction: var(--direction);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
padding-left var(--ha-animation-base-duration) ease,
|
||||
padding-inline-start var(--ha-animation-base-duration) ease;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-drawer,
|
||||
.mdc-drawer-app-content {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { updateAreaRegistryEntry } from "../data/area_registry";
|
||||
import { updateAreaRegistryEntry } from "../data/area/area_registry";
|
||||
import type {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiLabel, mdiPlus } from "@mdi/js";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
@@ -25,11 +25,9 @@ import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
const NO_LABELS = "___NO_LABELS___";
|
||||
|
||||
@customElement("ha-label-picker")
|
||||
export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -108,52 +106,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
private _labelMap = memoizeOne(
|
||||
(
|
||||
labels: LabelRegistryEntry[] | undefined
|
||||
): Map<string, LabelRegistryEntry> => {
|
||||
if (!labels) {
|
||||
return new Map();
|
||||
}
|
||||
return new Map(labels.map((label) => [label.label_id, label]));
|
||||
}
|
||||
);
|
||||
|
||||
private _computeValueRenderer = memoizeOne(
|
||||
(labels: LabelRegistryEntry[] | undefined): PickerValueRenderer =>
|
||||
(value) => {
|
||||
const label = this._labelMap(labels).get(value);
|
||||
|
||||
if (!label) {
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>
|
||||
<span slot="headline">${value}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${label.icon
|
||||
? html`<ha-icon slot="start" .icon=${label.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>`}
|
||||
<span slot="headline">${label.name}</span>
|
||||
`;
|
||||
}
|
||||
);
|
||||
|
||||
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||
|
||||
private _getItems = () => {
|
||||
if (!this._labels || this._labels.length === 0) {
|
||||
return [
|
||||
{
|
||||
id: NO_LABELS,
|
||||
primary: this.hass.localize("ui.components.label-picker.no_labels"),
|
||||
icon_path: mdiLabel,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return this._getLabelsMemoized(
|
||||
private _getItems = () =>
|
||||
this._getLabelsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
@@ -166,7 +122,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
this.entityFilter,
|
||||
this.excludeLabels
|
||||
);
|
||||
};
|
||||
|
||||
private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => {
|
||||
if (!labels) {
|
||||
@@ -219,8 +174,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.label-picker.label");
|
||||
|
||||
const valueRenderer = this._computeValueRenderer(this._labels);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.disabled=${this.disabled}
|
||||
@@ -237,7 +190,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.searchKeys=${labelComboBoxKeys}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
@@ -251,10 +203,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (value === NO_LABELS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
this._setValue(undefined);
|
||||
return;
|
||||
|
||||
@@ -138,10 +138,10 @@ export class HaMarkdown extends LitElement {
|
||||
--markdown-table-padding-inline: 0;
|
||||
--markdown-table-padding-block: 0;
|
||||
th {
|
||||
vertical-align: attr(align, center);
|
||||
vertical-align: attr(valign, middle);
|
||||
}
|
||||
td {
|
||||
vertical-align: attr(align, left);
|
||||
vertical-align: attr(valign, middle);
|
||||
}
|
||||
}
|
||||
table {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
|
||||
import { mdiClose, mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
@@ -26,6 +26,8 @@ import "./chips/ha-chip-set";
|
||||
import "./chips/ha-filter-chip";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@@ -147,7 +149,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
||||
|
||||
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
|
||||
@property({ type: Boolean, reflect: true }) public clearable = false;
|
||||
|
||||
@query("lit-virtualizer") public virtualizerElement?: LitVirtualizer;
|
||||
|
||||
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||
|
||||
@@ -160,7 +164,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this._virtualizerElement as HTMLElement | null;
|
||||
return this.virtualizerElement as HTMLElement | null;
|
||||
}
|
||||
|
||||
@state() private _sectionTitle?: string;
|
||||
@@ -207,8 +211,17 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
return html`<ha-textfield
|
||||
.label=${searchLabel}
|
||||
@blur=${this._resetSelectedItem}
|
||||
@input=${this._filterChanged}
|
||||
></ha-textfield>
|
||||
.iconTrailing=${this.clearable && !!this._search}
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._clearSearch}
|
||||
slot="trailingIcon"
|
||||
.label=${this.hass?.localize("ui.common.clear") || "Clear"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</ha-textfield>
|
||||
${this._renderSectionButtons()}
|
||||
${this.sections?.length
|
||||
? html`
|
||||
@@ -244,6 +257,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
@unpinned=${this._handleUnpinned}
|
||||
@scroll=${this._onScrollList}
|
||||
@focus=${this._focusList}
|
||||
@blur=${this._resetSelectedItem}
|
||||
@visibilityChanged=${this._visibilityChanged}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
@@ -276,18 +290,18 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
@eventOptions({ passive: true })
|
||||
private _visibilityChanged(ev) {
|
||||
if (
|
||||
this._virtualizerElement &&
|
||||
this.virtualizerElement &&
|
||||
this.sectionTitleFunction &&
|
||||
this.sections?.length
|
||||
) {
|
||||
const firstItem = this._virtualizerElement.items[ev.first];
|
||||
const secondItem = this._virtualizerElement.items[ev.first + 1];
|
||||
const firstItem = this.virtualizerElement.items[ev.first];
|
||||
const secondItem = this.virtualizerElement.items[ev.first + 1];
|
||||
this._sectionTitle = this.sectionTitleFunction({
|
||||
firstIndex: ev.first,
|
||||
lastIndex: ev.last,
|
||||
firstItem: firstItem as PickerComboBoxItem,
|
||||
secondItem: secondItem as PickerComboBoxItem,
|
||||
itemsCount: this._virtualizerElement.items.length,
|
||||
itemsCount: this.virtualizerElement.items.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -403,9 +417,22 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _valueSelected = (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
const value = (ev.currentTarget as any).value as string;
|
||||
const index = Number((ev.currentTarget as any).index);
|
||||
const newValue = value?.trim();
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
this._fireSelectedEvents(newValue, index);
|
||||
};
|
||||
|
||||
private _fireSelectedEvents(value: string, index: number) {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "index-selected", { index });
|
||||
}
|
||||
|
||||
private _clearSearch = () => {
|
||||
if (this._searchFieldElement) {
|
||||
this._searchFieldElement.value = "";
|
||||
this._searchFieldElement.dispatchEvent(new Event("input"));
|
||||
}
|
||||
};
|
||||
|
||||
private _fuseIndex = memoizeOne(
|
||||
@@ -487,8 +514,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this._items = this._getItems();
|
||||
|
||||
// Reset scroll position when filter changes
|
||||
if (this._virtualizerElement) {
|
||||
this._virtualizerElement.scrollToIndex(0);
|
||||
if (this.virtualizerElement) {
|
||||
this.virtualizerElement.scrollToIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,13 +538,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _selectNextItem = (ev?: KeyboardEvent) => {
|
||||
ev?.stopPropagation();
|
||||
ev?.preventDefault();
|
||||
if (!this._virtualizerElement) {
|
||||
if (!this.virtualizerElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._searchFieldElement?.focus();
|
||||
|
||||
const items = this._virtualizerElement.items as PickerComboBoxItem[];
|
||||
const items = this.virtualizerElement.items as PickerComboBoxItem[];
|
||||
|
||||
const maxItems = items.length - 1;
|
||||
|
||||
@@ -551,14 +578,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _selectPreviousItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if (!this._virtualizerElement) {
|
||||
if (!this.virtualizerElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._selectedItemIndex > 0) {
|
||||
const nextIndex = this._selectedItemIndex - 1;
|
||||
|
||||
const items = this._virtualizerElement.items as PickerComboBoxItem[];
|
||||
const items = this.virtualizerElement.items as PickerComboBoxItem[];
|
||||
|
||||
if (!items[nextIndex]) {
|
||||
return;
|
||||
@@ -580,13 +607,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _selectFirstItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
|
||||
if (!this.virtualizerElement || !this.virtualizerElement.items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = 0;
|
||||
|
||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||
if (typeof this.virtualizerElement.items[nextIndex] === "string") {
|
||||
this._selectedItemIndex = nextIndex + 1;
|
||||
} else {
|
||||
this._selectedItemIndex = nextIndex;
|
||||
@@ -597,13 +624,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _selectLastItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
|
||||
if (!this.virtualizerElement || !this.virtualizerElement.items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = this._virtualizerElement.items.length - 1;
|
||||
const nextIndex = this.virtualizerElement.items.length - 1;
|
||||
|
||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||
if (typeof this.virtualizerElement.items[nextIndex] === "string") {
|
||||
this._selectedItemIndex = nextIndex - 1;
|
||||
} else {
|
||||
this._selectedItemIndex = nextIndex;
|
||||
@@ -613,14 +640,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
};
|
||||
|
||||
private _scrollToSelectedItem = () => {
|
||||
this._virtualizerElement
|
||||
this.virtualizerElement
|
||||
?.querySelector(".selected")
|
||||
?.classList.remove("selected");
|
||||
|
||||
this._virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
|
||||
this.virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this._virtualizerElement
|
||||
this.virtualizerElement
|
||||
?.querySelector(`#list-item-${this._selectedItemIndex}`)
|
||||
?.classList.add("selected");
|
||||
});
|
||||
@@ -628,12 +655,20 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _pickSelectedItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
|
||||
|
||||
if (this._virtualizerElement?.items.length === 1) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: firstItem.id,
|
||||
if (
|
||||
this.virtualizerElement?.items?.length !== undefined &&
|
||||
this.virtualizerElement.items.length < 4 && // it still can have a section title and a padding item
|
||||
this.virtualizerElement.items.filter((item) => typeof item !== "string")
|
||||
.length === 1
|
||||
) {
|
||||
(
|
||||
this.virtualizerElement?.items as (PickerComboBoxItem | string)[]
|
||||
).forEach((item, index) => {
|
||||
if (typeof item !== "string") {
|
||||
this._fireSelectedEvents(item.id, index);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._selectedItemIndex === -1) {
|
||||
@@ -643,16 +678,16 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
// if filter button is focused
|
||||
ev.preventDefault();
|
||||
|
||||
const item = this._virtualizerElement?.items[
|
||||
const item = this.virtualizerElement?.items[
|
||||
this._selectedItemIndex
|
||||
] as PickerComboBoxItem;
|
||||
if (item) {
|
||||
fireEvent(this, "value-changed", { value: item.id });
|
||||
this._fireSelectedEvents(item.id, this._selectedItemIndex);
|
||||
}
|
||||
};
|
||||
|
||||
private _resetSelectedItem() {
|
||||
this._virtualizerElement
|
||||
this.virtualizerElement
|
||||
?.querySelector(".selected")
|
||||
?.classList.remove("selected");
|
||||
this._selectedItemIndex = -1;
|
||||
@@ -662,11 +697,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
typeof item === "string" ? item : item?.id;
|
||||
|
||||
private _getInitialSelectedIndex() {
|
||||
if (!this._virtualizerElement || this._search || !this.value) {
|
||||
if (!this.virtualizerElement || this._search || !this.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const index = this._virtualizerElement.items.findIndex(
|
||||
const index = this.virtualizerElement.items.findIndex(
|
||||
(item) =>
|
||||
typeof item !== "string" &&
|
||||
(item as PickerComboBoxItem).id === this.value
|
||||
@@ -691,6 +726,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:host([clearable]) {
|
||||
--text-field-padding: 0 0 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
padding: 0 var(--ha-space-3);
|
||||
margin-bottom: var(--ha-space-3);
|
||||
@@ -792,8 +831,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
.section-title,
|
||||
.title {
|
||||
box-sizing: border-box;
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-2) var(--ha-space-3);
|
||||
padding: var(--ha-space-1) var(--ha-space-4);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
min-height: var(--ha-space-6);
|
||||
@@ -822,7 +862,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
width: calc(100% - var(--ha-space-8));
|
||||
width: calc(100% - var(--ha-space-4));
|
||||
}
|
||||
|
||||
.section-title.show {
|
||||
@@ -846,4 +886,8 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-picker-combo-box": HaPickerComboBox;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"index-selected": { index: number };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +223,6 @@ export class HaRelatedItems extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain: entry.domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
@@ -249,7 +248,6 @@ export class HaRelatedItems extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain: integration,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
|
||||
@@ -87,7 +87,6 @@ export class HaMediaSelector extends LitElement {
|
||||
this._thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnail),
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -37,6 +37,7 @@ export class HaSelectorUiStateContent extends SubscribeMixin(LitElement) {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.allowName=${this.selector.ui_state_content?.allow_name || false}
|
||||
.allowContext=${this.selector.ui_state_content?.allow_context || false}
|
||||
></ha-entity-state-content-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -589,10 +589,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
// On keypresses on the listbox, we're going to ignore mouse enter events
|
||||
// for 100ms so that we ignore it when pressing down arrow scrolls the
|
||||
// sidebar causing the mouse to hover a new icon
|
||||
if (
|
||||
this.alwaysExpand ||
|
||||
new Date().getTime() < this._recentKeydownActiveUntil
|
||||
) {
|
||||
if (new Date().getTime() < this._recentKeydownActiveUntil) {
|
||||
return;
|
||||
}
|
||||
if (this._mouseLeaveTimeout) {
|
||||
@@ -612,7 +609,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _listboxFocusIn(ev) {
|
||||
if (this.alwaysExpand || ev.target.localName !== "ha-md-list-item") {
|
||||
if (ev.target.localName !== "ha-md-list-item") {
|
||||
return;
|
||||
}
|
||||
this._showTooltip(ev.target);
|
||||
@@ -652,6 +649,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
clearTimeout(this._tooltipHideTimeout);
|
||||
this._tooltipHideTimeout = undefined;
|
||||
}
|
||||
const itemText = item.querySelector(".item-text") as HTMLElement | null;
|
||||
if (this.hasAttribute("expanded") && itemText) {
|
||||
const isTruncated = itemText.scrollWidth > itemText.clientWidth;
|
||||
if (!isTruncated) {
|
||||
this._hideTooltip();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const tooltip = this._tooltip;
|
||||
const allListbox = this.shadowRoot!.querySelectorAll("ha-md-list")!;
|
||||
const listbox = [...allListbox].find((lb) => lb.contains(item));
|
||||
@@ -662,9 +667,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
(listbox?.offsetTop ?? 0) -
|
||||
(listbox?.scrollTop ?? 0);
|
||||
|
||||
tooltip.innerText = (
|
||||
item.querySelector(".item-text") as HTMLElement
|
||||
).innerText;
|
||||
tooltip.innerText = itemText?.innerText ?? "";
|
||||
tooltip.style.display = "block";
|
||||
tooltip.style.position = "fixed";
|
||||
tooltip.style.top = `${top}px`;
|
||||
@@ -728,6 +731,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
font-size: var(--ha-font-size-xl);
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
width: calc(56px + var(--safe-area-inset-left, 0px));
|
||||
padding-left: calc(
|
||||
var(--ha-space-1) + var(--safe-area-inset-left, 0px)
|
||||
);
|
||||
@@ -736,6 +741,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
padding-inline-end: initial;
|
||||
padding-top: var(--safe-area-inset-top, 0px);
|
||||
transition: width var(--ha-animation-base-duration) ease;
|
||||
}
|
||||
:host([expanded]) .menu {
|
||||
width: calc(256px + var(--safe-area-inset-left, 0px));
|
||||
@@ -750,15 +756,28 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
margin-left: 3px;
|
||||
margin-inline-start: 3px;
|
||||
margin-inline-end: initial;
|
||||
width: 100%;
|
||||
display: none;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
transform: translateX(-4px);
|
||||
transition:
|
||||
max-width var(--ha-animation-base-duration) ease,
|
||||
opacity var(--ha-animation-base-duration) ease,
|
||||
transform var(--ha-animation-base-duration) ease;
|
||||
}
|
||||
:host([narrow]) .title {
|
||||
margin: 0;
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
:host([expanded]) .title {
|
||||
display: initial;
|
||||
max-width: 100%;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
transition-delay: 0ms, 80ms, 80ms;
|
||||
}
|
||||
:host(:not([expanded])) .title {
|
||||
margin: 0;
|
||||
}
|
||||
.hidden-panel {
|
||||
display: none;
|
||||
@@ -800,6 +819,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
--md-list-item-leading-space: var(--ha-space-3);
|
||||
--md-list-item-trailing-space: var(--ha-space-3);
|
||||
--md-list-item-leading-icon-size: var(--ha-space-6);
|
||||
transition: width var(--ha-animation-base-duration) ease;
|
||||
}
|
||||
:host([expanded]) ha-md-list-item {
|
||||
width: 248px;
|
||||
@@ -840,12 +860,29 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
ha-md-list-item .item-text {
|
||||
display: none;
|
||||
display: block;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
transform: translateX(-4px);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
transition:
|
||||
max-width var(--ha-animation-base-duration) ease,
|
||||
opacity var(--ha-animation-base-duration) ease,
|
||||
transform var(--ha-animation-base-duration) ease;
|
||||
}
|
||||
:host([expanded]) ha-md-list-item .item-text {
|
||||
max-width: 100%;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
transition-delay: 0ms, 80ms, 80ms;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.divider {
|
||||
@@ -913,7 +950,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
position: absolute;
|
||||
opacity: 0.9;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
white-space: nowrap;
|
||||
max-width: calc(var(--ha-space-20) * 3);
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
color: var(--sidebar-background-color);
|
||||
background-color: var(--sidebar-text-color);
|
||||
padding: var(--ha-space-1);
|
||||
@@ -924,6 +963,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
-webkit-transform: scaleX(var(--scale-direction));
|
||||
transform: scaleX(var(--scale-direction));
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.menu,
|
||||
ha-md-list-item,
|
||||
ha-md-list-item .item-text,
|
||||
.title {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import type WaDialog from "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -106,6 +106,9 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||
public flexContent = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "without-header" })
|
||||
public withoutHeader = false;
|
||||
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
@@ -147,29 +150,35 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@wa-after-show=${this._handleAfterShow}
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
>
|
||||
<slot name="header">
|
||||
<ha-dialog-header
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
.showBorder=${this._bodyScrolled}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>
|
||||
${!this.withoutHeader
|
||||
? html` <slot name="header">
|
||||
<ha-dialog-header
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
.showBorder=${this._bodyScrolled}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span
|
||||
slot="title"
|
||||
class="title"
|
||||
id="ha-wa-dialog-title"
|
||||
>
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>`
|
||||
: nothing}
|
||||
<div class="content-wrapper">
|
||||
<div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
|
||||
<slot></slot>
|
||||
|
||||
@@ -793,7 +793,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
import { getConfigEntry } from "../../data/config_entries";
|
||||
import { labelsContext } from "../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
|
||||
204
src/data/area/area_picker.ts
Normal file
204
src/data/area/area_picker.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../../common/entity/context/get_area_context";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
getDeviceEntityDisplayLookup,
|
||||
type DeviceEntityDisplayLookup,
|
||||
type DeviceRegistryEntry,
|
||||
} from "../device/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../entity/entity_registry";
|
||||
|
||||
export const getAreas = (
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
haStates: HomeAssistant["states"],
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
idPrefix = ""
|
||||
): PickerComboBoxItem[] => {
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
const areas = Object.values(haAreas);
|
||||
const devices = Object.values(haDevices);
|
||||
const entities = Object.values(haEntities);
|
||||
|
||||
if (
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses ||
|
||||
deviceFilter ||
|
||||
entityFilter
|
||||
) {
|
||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter(stateObj);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter!(stateObj);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outputAreas = areas;
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
areaIds = inputDevices
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.area_id!);
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
areaIds = (areaIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.area_id)
|
||||
.map((entity) => entity.area_id!)
|
||||
);
|
||||
}
|
||||
|
||||
if (areaIds) {
|
||||
outputAreas = outputAreas.filter((area) => areaIds!.includes(area.area_id));
|
||||
}
|
||||
|
||||
if (excludeAreas) {
|
||||
outputAreas = outputAreas.filter(
|
||||
(area) => !excludeAreas!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
const items = outputAreas.map<PickerComboBoxItem>((area) => {
|
||||
const { floor } = getAreaContext(area, haFloors);
|
||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
||||
const areaName = computeAreaName(area);
|
||||
return {
|
||||
id: `${idPrefix}${area.area_id}`,
|
||||
primary: areaName || area.area_id,
|
||||
secondary: floorName,
|
||||
icon: area.icon || undefined,
|
||||
icon_path: area.icon ? undefined : mdiTextureBox,
|
||||
search_labels: {
|
||||
areaId: area.area_id,
|
||||
aliases: area.aliases.join(" "),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export const areaComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "search_labels.aliases",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "secondary",
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
name: "search_labels.domain",
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
name: "search_labels.areaId",
|
||||
weight: 2,
|
||||
},
|
||||
];
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { DeviceRegistryEntry } from "./device/device_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DeviceRegistryEntry } from "../device/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "./entity/entity_registry";
|
||||
import type { RegistryEntry } from "./registry";
|
||||
} from "../entity/entity_registry";
|
||||
import type { RegistryEntry } from "../registry";
|
||||
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
export { subscribeAreaRegistry } from "../ws-area_registry";
|
||||
|
||||
export interface AreaRegistryEntry extends RegistryEntry {
|
||||
aliases: string[];
|
||||
@@ -6,7 +6,7 @@ import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-dev
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
import {
|
||||
getDeviceEntityDisplayLookup,
|
||||
type DeviceEntityDisplayLookup,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
import type { RegistryEntry } from "./registry";
|
||||
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
|
||||
/** Panel to show when no panel is picked. */
|
||||
export const DEFAULT_PANEL = "lovelace";
|
||||
@@ -72,6 +74,40 @@ export const getPanelTitleFromUrlPath = (
|
||||
return getPanelTitle(hass, panel);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get subpage title for config panel routes.
|
||||
* Returns the specific subpage title (e.g., "Automations") if found,
|
||||
* or undefined to fall back to the panel title (e.g., "Settings").
|
||||
*
|
||||
* @param hass HomeAssistant instance
|
||||
* @param path Full route path (e.g., "/config/automation/dashboard")
|
||||
* @param configSections Config sections metadata for resolving subpage titles
|
||||
* @returns Localized subpage title, or undefined if not found
|
||||
*/
|
||||
export const getConfigSubpageTitle = (
|
||||
hass: HomeAssistant,
|
||||
path: string,
|
||||
configSections: Record<string, PageNavigation[]>
|
||||
): string | undefined => {
|
||||
// Search through all config section groups for a matching path
|
||||
for (const sectionGroup of Object.values(configSections)) {
|
||||
const pageNav = sectionGroup.find((nav) => path.startsWith(nav.path));
|
||||
if (pageNav) {
|
||||
if (pageNav.translationKey) {
|
||||
const localized = hass.localize(pageNav.translationKey as LocalizeKeys);
|
||||
if (localized) {
|
||||
return localized;
|
||||
}
|
||||
}
|
||||
|
||||
if (pageNav.name) {
|
||||
return pageNav.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getPanelIcon = (panel: PanelInfo): string | undefined => {
|
||||
if (!panel.icon) {
|
||||
switch (panel.component_name) {
|
||||
|
||||
327
src/data/quick_bar.ts
Normal file
327
src/data/quick_bar.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import {
|
||||
mdiKeyboard,
|
||||
mdiNavigationVariant,
|
||||
mdiPuzzle,
|
||||
mdiReload,
|
||||
mdiServerNetwork,
|
||||
mdiStorePlus,
|
||||
} from "@mdi/js";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import { componentsWithService } from "../common/config/components_with_service";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import { configSections } from "../panels/config/ha-panel-config";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HassioAddonInfo } from "./hassio/addon";
|
||||
import { domainToName } from "./integration";
|
||||
import { getPanelIcon, getPanelNameTranslationKey } from "./panel";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
|
||||
export interface NavigationComboBoxItem extends PickerComboBoxItem {
|
||||
path: string;
|
||||
image?: string;
|
||||
iconColor?: string;
|
||||
}
|
||||
|
||||
export interface BaseNavigationCommand {
|
||||
path: string;
|
||||
primary: string;
|
||||
icon_path?: string;
|
||||
iconPath?: string;
|
||||
iconColor?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface ActionCommandComboBoxItem extends PickerComboBoxItem {
|
||||
action: string;
|
||||
domain?: string;
|
||||
}
|
||||
|
||||
export interface NavigationInfo extends PageNavigation {
|
||||
primary: string;
|
||||
}
|
||||
|
||||
const generateNavigationPanelCommands = (
|
||||
localize: HomeAssistant["localize"],
|
||||
panels: HomeAssistant["panels"],
|
||||
addons?: HassioAddonInfo[]
|
||||
): BaseNavigationCommand[] =>
|
||||
Object.entries(panels)
|
||||
.filter(
|
||||
([panelKey]) => panelKey !== "_my_redirect" && panelKey !== "hassio"
|
||||
)
|
||||
.map(([_panelKey, panel]) => {
|
||||
const translationKey = getPanelNameTranslationKey(panel);
|
||||
const icon = getPanelIcon(panel) || "mdi:view-dashboard";
|
||||
|
||||
const primary = localize(translationKey) || panel.title || panel.url_path;
|
||||
|
||||
let image: string | undefined;
|
||||
|
||||
if (addons) {
|
||||
const addon = addons.find(({ slug }) => slug === panel.url_path);
|
||||
if (addon) {
|
||||
image = addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
primary,
|
||||
icon,
|
||||
image,
|
||||
path: `/${panel.url_path}`,
|
||||
};
|
||||
});
|
||||
|
||||
const getNavigationInfoFromConfig = (
|
||||
localize: HomeAssistant["localize"],
|
||||
page: PageNavigation
|
||||
): NavigationInfo | undefined => {
|
||||
const path = page.path.substring(1);
|
||||
|
||||
let name = path.substring(path.indexOf("/") + 1);
|
||||
name = name.indexOf("/") > -1 ? name.substring(0, name.indexOf("/")) : name;
|
||||
|
||||
const caption =
|
||||
(name && localize(`ui.dialogs.quick-bar.commands.navigation.${name}`)) ||
|
||||
// @ts-expect-error
|
||||
(page.translationKey && localize(page.translationKey));
|
||||
|
||||
if (caption) {
|
||||
return { ...page, primary: caption };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const generateNavigationConfigSectionCommands = (
|
||||
hass: HomeAssistant
|
||||
): BaseNavigationCommand[] => {
|
||||
if (!hass.user?.is_admin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items: NavigationInfo[] = [];
|
||||
|
||||
Object.values(configSections).forEach((sectionPages) => {
|
||||
sectionPages.forEach((page) => {
|
||||
if (!canShowPage(hass, page)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = getNavigationInfoFromConfig(hass.localize, page);
|
||||
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
// Add to list, but only if we do not already have an entry for the same path and component
|
||||
if (items.some((e) => e.path === info.path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.push(info);
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const finalizeNavigationCommands = (
|
||||
localize: HomeAssistant["localize"],
|
||||
items: BaseNavigationCommand[]
|
||||
): NavigationComboBoxItem[] =>
|
||||
items.map((item, index) => {
|
||||
const secondary = localize(
|
||||
"ui.dialogs.quick-bar.commands.types.navigation"
|
||||
);
|
||||
return {
|
||||
id: `navigation_${index}_${item.path}`,
|
||||
icon_path: item.iconPath || mdiNavigationVariant,
|
||||
secondary,
|
||||
sorting_label: `${item.primary}_${secondary}`,
|
||||
...item,
|
||||
};
|
||||
});
|
||||
|
||||
export const generateNavigationCommands = (
|
||||
hass: HomeAssistant,
|
||||
addons?: HassioAddonInfo[]
|
||||
): NavigationComboBoxItem[] => {
|
||||
const panelItems = generateNavigationPanelCommands(
|
||||
hass.localize,
|
||||
hass.panels,
|
||||
addons
|
||||
);
|
||||
const sectionItems = generateNavigationConfigSectionCommands(hass);
|
||||
const supervisorItems: BaseNavigationCommand[] = [];
|
||||
if (hass.user?.is_admin && isComponentLoaded(hass, "hassio")) {
|
||||
supervisorItems.push({
|
||||
path: "/hassio/store",
|
||||
icon_path: mdiStorePlus,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_store"
|
||||
),
|
||||
});
|
||||
supervisorItems.push({
|
||||
path: "/hassio/dashboard",
|
||||
icon_path: mdiPuzzle,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_dashboard"
|
||||
),
|
||||
});
|
||||
if (addons) {
|
||||
for (const addon of addons.filter((a) => a.version)) {
|
||||
supervisorItems.push({
|
||||
path: `/hassio/addon/${addon.slug}`,
|
||||
image: addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_info",
|
||||
{ addon: addon.name }
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const additionalItems = [
|
||||
{
|
||||
path: "",
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.shortcuts"
|
||||
),
|
||||
icon_path: mdiKeyboard,
|
||||
},
|
||||
];
|
||||
|
||||
return finalizeNavigationCommands(hass.localize, [
|
||||
...panelItems,
|
||||
...sectionItems,
|
||||
...supervisorItems,
|
||||
...additionalItems,
|
||||
]);
|
||||
};
|
||||
|
||||
const generateReloadCommands = (
|
||||
hass: HomeAssistant
|
||||
): ActionCommandComboBoxItem[] => {
|
||||
// Get all domains that have a direct "reload" service
|
||||
const reloadableDomains = componentsWithService(hass, "reload");
|
||||
|
||||
const commands = reloadableDomains.map((domain) => ({
|
||||
primary:
|
||||
hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
|
||||
hass.localize("ui.dialogs.quick-bar.commands.reload.reload", {
|
||||
domain: domainToName(hass.localize, domain),
|
||||
}),
|
||||
domain,
|
||||
action: "reload",
|
||||
icon_path: mdiReload,
|
||||
secondary: hass.localize(`ui.dialogs.quick-bar.commands.types.reload`),
|
||||
}));
|
||||
|
||||
// Add "frontend.reload_themes"
|
||||
commands.push({
|
||||
primary: hass.localize("ui.dialogs.quick-bar.commands.reload.themes"),
|
||||
domain: "frontend",
|
||||
action: "reload_themes",
|
||||
icon_path: mdiReload,
|
||||
secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
|
||||
});
|
||||
|
||||
// Add "homeassistant.reload_core_config"
|
||||
commands.push({
|
||||
primary: hass.localize("ui.dialogs.quick-bar.commands.reload.core"),
|
||||
domain: "homeassistant",
|
||||
action: "reload_core_config",
|
||||
icon_path: mdiReload,
|
||||
secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
|
||||
});
|
||||
|
||||
// Add "homeassistant.reload_all"
|
||||
commands.push({
|
||||
primary: hass.localize("ui.dialogs.quick-bar.commands.reload.all"),
|
||||
domain: "homeassistant",
|
||||
action: "reload_all",
|
||||
icon_path: mdiReload,
|
||||
secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
|
||||
});
|
||||
|
||||
return commands.map((command, index) => ({
|
||||
...command,
|
||||
id: `command_${index}_${command.primary}`,
|
||||
sorting_label: `${command.primary}_${command.secondary}_${command.domain}`,
|
||||
}));
|
||||
};
|
||||
|
||||
const generateServerControlCommands = (
|
||||
hass: HomeAssistant
|
||||
): ActionCommandComboBoxItem[] => {
|
||||
const serverActions = ["restart", "stop"] as const;
|
||||
|
||||
return serverActions.map((action, index) => {
|
||||
const primary = hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
{
|
||||
action: hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
const secondary = hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.types.server_control"
|
||||
);
|
||||
|
||||
return {
|
||||
id: `server_control_${index}_${action}`,
|
||||
primary,
|
||||
domain: "homeassistant",
|
||||
icon_path: mdiServerNetwork,
|
||||
secondary,
|
||||
sorting_label: `${primary}_${secondary}_${action}`,
|
||||
action,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const generateActionCommands = (
|
||||
hass: HomeAssistant
|
||||
): ActionCommandComboBoxItem[] => [
|
||||
...generateReloadCommands(hass),
|
||||
...generateServerControlCommands(hass),
|
||||
];
|
||||
|
||||
export const commandComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "domain",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "secondary",
|
||||
weight: 6,
|
||||
},
|
||||
];
|
||||
|
||||
export const navigateComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "secondary",
|
||||
weight: 6,
|
||||
},
|
||||
];
|
||||
@@ -377,7 +377,7 @@ interface SelectBoxOptionImage {
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
value: any;
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
image?: string | SelectBoxOptionImage;
|
||||
@@ -501,6 +501,7 @@ export interface UiStateContentSelector {
|
||||
ui_state_content: {
|
||||
entity_id?: string;
|
||||
allow_name?: boolean;
|
||||
allow_context?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
import type { FloorComboBoxItem } from "./area_floor_picker";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { DevicePickerItem } from "./device/device_picker";
|
||||
import type { DeviceRegistryEntry } from "./device/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity/entity";
|
||||
|
||||
18
src/data/theme.ts
Normal file
18
src/data/theme.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { HomeAssistant, ThemeSettings } from "../types";
|
||||
import { saveFrontendUserData, subscribeFrontendUserData } from "./frontend";
|
||||
|
||||
declare global {
|
||||
interface FrontendUserData {
|
||||
theme: ThemeSettings;
|
||||
}
|
||||
}
|
||||
|
||||
export const subscribeThemePreferences = (
|
||||
hass: HomeAssistant,
|
||||
callback: (data: { value: ThemeSettings | null }) => void
|
||||
) => subscribeFrontendUserData(hass.connection, "theme", callback);
|
||||
|
||||
export const saveThemePreferences = (
|
||||
hass: HomeAssistant,
|
||||
data: ThemeSettings
|
||||
) => saveFrontendUserData(hass.connection, "theme", data);
|
||||
@@ -2,7 +2,7 @@ import type { Connection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
|
||||
const fetchAreaRegistry = (conn: Connection) =>
|
||||
conn.sendMessagePromise<AreaRegistryEntry[]>({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,15 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export const enum QuickBarMode {
|
||||
Command = "command",
|
||||
Device = "device",
|
||||
Entity = "entity",
|
||||
}
|
||||
export type QuickBarSection =
|
||||
| "entity"
|
||||
| "device"
|
||||
| "area"
|
||||
| "navigate"
|
||||
| "command";
|
||||
|
||||
export interface QuickBarParams {
|
||||
entityFilter?: string;
|
||||
mode?: QuickBarMode;
|
||||
mode?: QuickBarSection;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import "../../components/ha-alert";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-svg-icon";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import "../../components/ha-wa-dialog";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
|
||||
@@ -38,6 +37,10 @@ const _SHORTCUTS: Section[] = [
|
||||
{
|
||||
textTranslationKey: "ui.dialogs.shortcuts.searching.on_any_page",
|
||||
},
|
||||
{
|
||||
shortcut: [CTRL_CMD, "K"],
|
||||
descriptionTranslationKey: "ui.dialogs.shortcuts.searching.search",
|
||||
},
|
||||
{
|
||||
shortcut: ["C"],
|
||||
descriptionTranslationKey:
|
||||
@@ -165,17 +168,22 @@ const _SHORTCUTS: Section[] = [
|
||||
class DialogShortcuts extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _opened = false;
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(): Promise<void> {
|
||||
this._opened = true;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public async closeDialog(): Promise<void> {
|
||||
this._opened = false;
|
||||
private _dialogClosed() {
|
||||
this._open = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public async closeDialog() {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _renderShortcut(
|
||||
shortcutKeys: ShortcutString[],
|
||||
descriptionKey: LocalizeKeys
|
||||
@@ -202,20 +210,11 @@ class DialogShortcuts extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
defaultAction="ignore"
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.dialogs.shortcuts.title")
|
||||
)}
|
||||
<ha-wa-dialog
|
||||
.open=${this._open}
|
||||
@closed=${this._dialogClosed}
|
||||
.headerTitle=${this.hass.localize("ui.dialogs.shortcuts.title")}
|
||||
>
|
||||
<div class="content">
|
||||
${_SHORTCUTS.map(
|
||||
@@ -238,7 +237,7 @@ class DialogShortcuts extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ha-alert>
|
||||
<ha-alert slot="footer">
|
||||
${this.hass.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
|
||||
user_profile: html`<a href="/profile/general#shortcuts"
|
||||
>${this.hass.localize(
|
||||
@@ -247,25 +246,12 @@ class DialogShortcuts extends LitElement {
|
||||
>`,
|
||||
})}
|
||||
</ha-alert>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 15;
|
||||
}
|
||||
|
||||
h3:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -287,6 +273,10 @@ class DialogShortcuts extends LitElement {
|
||||
ha-svg-icon {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
ha-alert a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -186,7 +186,11 @@ class PanelClimate extends LitElement {
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
z-index: 4;
|
||||
transition: box-shadow 200ms linear;
|
||||
transition:
|
||||
box-shadow 200ms linear,
|
||||
width var(--ha-animation-base-duration) ease,
|
||||
padding-left var(--ha-animation-base-duration) ease,
|
||||
padding-right var(--ha-animation-base-duration) ease;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
@@ -211,6 +215,11 @@ class PanelClimate extends LitElement {
|
||||
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
|
||||
);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.header {
|
||||
transition: box-shadow 200ms linear;
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
height: var(--header-height);
|
||||
display: flex;
|
||||
|
||||
@@ -21,8 +21,8 @@ import "../../../components/ha-wa-dialog";
|
||||
import type {
|
||||
AreaRegistryEntry,
|
||||
AreaRegistryEntryMutableParams,
|
||||
} from "../../../data/area_registry";
|
||||
import { deleteAreaRegistryEntry } from "../../../data/area_registry";
|
||||
} from "../../../data/area/area_registry";
|
||||
import { deleteAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import {
|
||||
SENSOR_DEVICE_CLASS_HUMIDITY,
|
||||
SENSOR_DEVICE_CLASS_TEMPERATURE,
|
||||
|
||||
@@ -14,16 +14,16 @@ import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-floor-icon";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import {
|
||||
reorderAreaRegistryEntries,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
} from "../../../data/area/area_registry";
|
||||
import { reorderFloorRegistryEntries } from "../../../data/floor_registry";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -18,7 +18,7 @@ import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import { updateAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { updateAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type {
|
||||
FloorRegistryEntry,
|
||||
FloorRegistryEntryMutableParams,
|
||||
|
||||
@@ -23,11 +23,11 @@ import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import {
|
||||
deleteAreaRegistryEntry,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
} from "../../../data/area/area_registry";
|
||||
import type { AutomationEntity } from "../../../data/automation";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
|
||||
@@ -31,12 +31,12 @@ import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-sortable";
|
||||
import type { HaSortableOptions } from "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import {
|
||||
createAreaRegistryEntry,
|
||||
reorderAreaRegistryEntries,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
} from "../../../data/area/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import {
|
||||
createFloorRegistryEntry,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type {
|
||||
AreaRegistryEntry,
|
||||
AreaRegistryEntryMutableParams,
|
||||
} from "../../../data/area_registry";
|
||||
} from "../../../data/area/area_registry";
|
||||
|
||||
export interface AreaRegistryDetailDialogParams {
|
||||
entry?: AreaRegistryEntry;
|
||||
|
||||
@@ -58,6 +58,7 @@ import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type {
|
||||
Action,
|
||||
DeviceAction,
|
||||
NonConditionAction,
|
||||
RepeatAction,
|
||||
ServiceAction,
|
||||
@@ -233,6 +234,13 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
private _renderRow() {
|
||||
const type = getAutomationActionType(this.action);
|
||||
|
||||
const target =
|
||||
type === "service" && "target" in this.action
|
||||
? (this.action as ServiceAction).target
|
||||
: type === "device_id" && (this.action as DeviceAction).device_id
|
||||
? { device_id: (this.action as DeviceAction).device_id }
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
${type === "service" && "action" in this.action && this.action.action
|
||||
? html`
|
||||
@@ -254,9 +262,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action)
|
||||
)}
|
||||
${type === "service" && "target" in this.action
|
||||
? this._renderTargets((this.action as ServiceAction).target)
|
||||
: nothing}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
@@ -857,6 +863,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
ev.stopPropagation();
|
||||
const action = ev.detail?.item?.value;
|
||||
|
||||
if (!action) {
|
||||
|
||||
@@ -57,11 +57,11 @@ import {
|
||||
ACTION_COLLECTIONS,
|
||||
ACTION_ICONS,
|
||||
} from "../../../data/action";
|
||||
import type { FloorComboBoxItem } from "../../../data/area_floor_picker";
|
||||
import {
|
||||
getAreaDeviceLookup,
|
||||
getAreaEntityLookup,
|
||||
} from "../../../data/area_registry";
|
||||
} from "../../../data/area/area_registry";
|
||||
import type { FloorComboBoxItem } from "../../../data/area_floor_picker";
|
||||
import {
|
||||
DYNAMIC_PREFIX,
|
||||
getValueFromDynamic,
|
||||
@@ -232,6 +232,8 @@ class DialogAddAutomationElement
|
||||
|
||||
private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||
|
||||
private _closing = false;
|
||||
|
||||
// #endregion variables
|
||||
|
||||
// #region lifecycle
|
||||
@@ -347,6 +349,8 @@ class DialogAddAutomationElement
|
||||
}
|
||||
}
|
||||
|
||||
this._closing = true;
|
||||
|
||||
// if dialog is closed, but root level isn't active, clean up history state
|
||||
if (mainWindow.history.state?.dialogData) {
|
||||
this._open = false;
|
||||
@@ -360,6 +364,7 @@ class DialogAddAutomationElement
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
this._open = true;
|
||||
this._closing = false;
|
||||
this._params = undefined;
|
||||
this._selectedCollectionIndex = undefined;
|
||||
this._selectedGroup = undefined;
|
||||
@@ -431,6 +436,24 @@ class DialogAddAutomationElement
|
||||
|
||||
// #region render
|
||||
|
||||
private _getEmptyNote(automationElementType: string) {
|
||||
if (
|
||||
automationElementType !== "trigger" &&
|
||||
automationElementType !== "condition"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.no_items_for_target_note`,
|
||||
{
|
||||
labs_link: html`<a href="/config/labs" @click=${this._close}
|
||||
>${this.hass.localize("ui.panel.config.labs.caption")}</a
|
||||
>`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
@@ -696,6 +719,7 @@ class DialogAddAutomationElement
|
||||
.emptyLabel=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.no_items_for_target`
|
||||
)}
|
||||
.emptyNote=${this._getEmptyNote(automationElementType)}
|
||||
.tooltipDescription=${this._tab === "targets"}
|
||||
.target=${(this._tab === "targets" &&
|
||||
this._selectedTarget &&
|
||||
@@ -1691,9 +1715,9 @@ class DialogAddAutomationElement
|
||||
|
||||
// #region interaction
|
||||
|
||||
private _close() {
|
||||
private _close = () => {
|
||||
this._open = false;
|
||||
}
|
||||
};
|
||||
|
||||
private _back() {
|
||||
mainWindow.history.back();
|
||||
@@ -1875,7 +1899,10 @@ class DialogAddAutomationElement
|
||||
}
|
||||
|
||||
private _handleClosed() {
|
||||
this.closeDialog();
|
||||
// if closing isn't already in progress, close the dialog
|
||||
if (!this._closing) {
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
// #region interaction
|
||||
|
||||
@@ -28,6 +28,10 @@ import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-section-title";
|
||||
import "../../../../components/ha-state-icon";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import {
|
||||
getAreaDeviceLookup,
|
||||
getAreaEntityLookup,
|
||||
} from "../../../../data/area/area_registry";
|
||||
import {
|
||||
getAreasNestedInFloors,
|
||||
type AreaFloorValue,
|
||||
@@ -35,10 +39,6 @@ import {
|
||||
type FloorNestedComboBoxItem,
|
||||
type UnassignedAreasFloorComboBoxItem,
|
||||
} from "../../../../data/area_floor_picker";
|
||||
import {
|
||||
getAreaDeviceLookup,
|
||||
getAreaEntityLookup,
|
||||
} from "../../../../data/area_registry";
|
||||
import {
|
||||
getConfigEntries,
|
||||
type ConfigEntry,
|
||||
@@ -1504,14 +1504,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
box-shadow: inset var(--ha-shadow-offset-x-lg)
|
||||
calc(var(--ha-shadow-offset-y-lg) * -1) var(--ha-shadow-blur-lg)
|
||||
var(--ha-shadow-spread-lg) var(--ha-color-shadow-light);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.targets-show-more {
|
||||
box-shadow: inset var(--ha-shadow-offset-x-lg)
|
||||
calc(var(--ha-shadow-offset-y-lg) * -1) var(--ha-shadow-blur-lg)
|
||||
var(--ha-shadow-spread-lg) var(--ha-color-shadow-dark);
|
||||
}
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px), all and (max-height: 500px) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mdiInformationOutline, mdiPlus } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -39,6 +39,8 @@ export class HaAutomationAddItems extends LitElement {
|
||||
|
||||
@property({ attribute: "empty-label" }) public emptyLabel!: string;
|
||||
|
||||
@property({ attribute: false }) public emptyNote?: string | TemplateResult;
|
||||
|
||||
@property({ attribute: false }) public target?: Target;
|
||||
|
||||
@property({ attribute: false }) public getLabel!: (
|
||||
@@ -79,6 +81,9 @@ export class HaAutomationAddItems extends LitElement {
|
||||
? html`${this.emptyLabel}
|
||||
${this.target
|
||||
? html`<div>${this._renderTarget(this.target)}</div>`
|
||||
: nothing}
|
||||
${this.emptyNote
|
||||
? html`<div class="empty-note">${this.emptyNote}</div>`
|
||||
: nothing}`
|
||||
: repeat(
|
||||
this.items,
|
||||
@@ -199,6 +204,7 @@ export class HaAutomationAddItems extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
:host([scrollable]) .items {
|
||||
overflow: auto;
|
||||
@@ -213,13 +219,24 @@ export class HaAutomationAddItems extends LitElement {
|
||||
background-color: var(--ha-color-surface-default);
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
padding: 0;
|
||||
padding: var(--ha-space-4);
|
||||
margin: 0 var(--ha-space-4)
|
||||
max(var(--safe-area-inset-bottom), var(--ha-space-3));
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-note {
|
||||
color: var(--ha-color-text-secondary);
|
||||
margin-top: var(--ha-space-2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-note a {
|
||||
color: currentColor;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.items.error {
|
||||
background-color: var(--ha-color-fill-danger-quiet-resting);
|
||||
color: var(--ha-color-on-danger-normal);
|
||||
|
||||
@@ -76,6 +76,7 @@ import "./types/ha-automation-condition-template";
|
||||
import "./types/ha-automation-condition-time";
|
||||
import "./types/ha-automation-condition-trigger";
|
||||
import "./types/ha-automation-condition-zone";
|
||||
import type { DeviceCondition } from "../../../../data/device/device_automation";
|
||||
|
||||
export interface ConditionElement extends LitElement {
|
||||
condition: Condition;
|
||||
@@ -184,6 +185,14 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderRow() {
|
||||
const target =
|
||||
"target" in (this.conditionDescriptions[this.condition.condition] || {})
|
||||
? (this.condition as PlatformCondition).target
|
||||
: "device_id" in this.condition &&
|
||||
(this.condition as DeviceCondition).device_id
|
||||
? { device_id: [(this.condition as DeviceCondition).device_id] }
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-condition-icon
|
||||
slot="leading-icon"
|
||||
@@ -194,10 +203,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
)}
|
||||
${"target" in
|
||||
(this.conditionDescriptions[this.condition.condition] || {})
|
||||
? this._renderTargets((this.condition as PlatformCondition).target)
|
||||
: nothing}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
@@ -833,6 +839,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
ev.stopPropagation();
|
||||
const action = ev.detail?.item?.value;
|
||||
|
||||
if (!action) {
|
||||
|
||||
@@ -67,7 +67,7 @@ import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { createAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { AutomationEntity } from "../../../data/automation";
|
||||
import {
|
||||
deleteAutomation,
|
||||
@@ -381,12 +381,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.automation.picker.headers.voice_assistants"
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
),
|
||||
type: "icon",
|
||||
type: "flex",
|
||||
defaultHidden: true,
|
||||
minWidth: "100px",
|
||||
maxWidth: "100px",
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
template: (automation) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
|
||||
@@ -350,6 +350,7 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
ev.stopPropagation();
|
||||
const action = ev.detail?.item?.value;
|
||||
|
||||
if (!action) {
|
||||
|
||||
@@ -56,6 +56,7 @@ import { isTrigger, subscribeTrigger } from "../../../../data/automation";
|
||||
import { describeTrigger } from "../../../../data/automation_i18n";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { DeviceTrigger } from "../../../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { TriggerDescriptions } from "../../../../data/trigger";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
@@ -196,6 +197,15 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
const yamlMode = this._yamlMode || !supported;
|
||||
|
||||
const target =
|
||||
type === "platform" &&
|
||||
"target" in
|
||||
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger]
|
||||
? (this.trigger as PlatformTrigger).target
|
||||
: type === "device" && (this.trigger as DeviceTrigger).device_id
|
||||
? { device_id: (this.trigger as DeviceTrigger).device_id }
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
${type === "list"
|
||||
? html`<ha-svg-icon
|
||||
@@ -210,11 +220,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
></ha-trigger-icon>`}
|
||||
<h3 slot="header">
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
${type === "platform" &&
|
||||
"target" in
|
||||
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger]
|
||||
? this._renderTargets((this.trigger as PlatformTrigger).target)
|
||||
: nothing}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
@@ -809,6 +815,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
ev.stopPropagation();
|
||||
const action = ev.detail?.item?.value;
|
||||
|
||||
if (!action) {
|
||||
|
||||
@@ -152,7 +152,6 @@ class HaBackupConfigAgents extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
|
||||
@@ -66,7 +66,6 @@ class HaBackupAgentsPicker extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
|
||||
@@ -226,7 +226,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
height="24"
|
||||
|
||||
@@ -207,7 +207,6 @@ class HaConfigBackupDetails extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized:
|
||||
this.hass.themes?.darkMode,
|
||||
})}
|
||||
|
||||
@@ -252,7 +252,6 @@ class HaConfigBackupSettings extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain: "cloud",
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
|
||||
@@ -4,10 +4,8 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -28,14 +26,21 @@ class DialogAssignCategory extends LitElement {
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: AssignCategoryDialogParams): void {
|
||||
this._params = params;
|
||||
this._scope = params.scope;
|
||||
this._category = params.entityReg.categories[params.scope];
|
||||
this._error = undefined;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
@@ -47,47 +52,46 @@ class DialogAssignCategory extends LitElement {
|
||||
}
|
||||
const entry = this._params.entityReg.categories[this._params.scope];
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
entry
|
||||
? this.hass.localize("ui.panel.config.category.assign.edit")
|
||||
: this.hass.localize("ui.panel.config.category.assign.assign")
|
||||
)}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${entry
|
||||
? this.hass.localize("ui.panel.config.category.assign.edit")
|
||||
: this.hass.localize("ui.panel.config.category.assign.assign")}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-category-picker
|
||||
.hass=${this.hass}
|
||||
.scope=${this._scope}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.category-picker.category"
|
||||
)}
|
||||
.value=${this._category}
|
||||
@value-changed=${this._categoryChanged}
|
||||
></ha-category-picker>
|
||||
</div>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-category-picker
|
||||
.hass=${this.hass}
|
||||
.scope=${this._scope}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.category-picker.category"
|
||||
)}
|
||||
.value=${this._category}
|
||||
@value-changed=${this._categoryChanged}
|
||||
autofocus
|
||||
></ha-category-picker>
|
||||
</div>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!!this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!!this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ 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 { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-textfield";
|
||||
import type {
|
||||
@@ -30,11 +30,14 @@ class DialogCategoryDetail extends LitElement {
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(
|
||||
params: CategoryRegistryDetailDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._open = true;
|
||||
if (this._params.entry) {
|
||||
this._name = this._params.entry.name || "";
|
||||
this._icon = this._params.entry.icon || null;
|
||||
@@ -46,6 +49,10 @@ class DialogCategoryDetail extends LitElement {
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
@@ -58,61 +65,55 @@ class DialogCategoryDetail extends LitElement {
|
||||
const entry = this._params.entry;
|
||||
const nameInvalid = !this._isNameValid();
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
entry
|
||||
? this.hass.localize("ui.panel.config.category.editor.edit")
|
||||
: this.hass.localize("ui.panel.config.category.editor.create")
|
||||
)}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${entry
|
||||
? this.hass.localize("ui.panel.config.category.editor.edit")
|
||||
: this.hass.localize("ui.panel.config.category.editor.create")}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-textfield
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.category.editor.name"
|
||||
)}
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.category.editor.required_error_msg"
|
||||
)}
|
||||
required
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-textfield
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass.localize("ui.panel.config.category.editor.name")}
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.category.editor.required_error_msg"
|
||||
)}
|
||||
required
|
||||
autofocus
|
||||
></ha-textfield>
|
||||
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._icon}
|
||||
@value-changed=${this._iconChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.category.editor.icon"
|
||||
)}
|
||||
></ha-icon-picker>
|
||||
</div>
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._icon}
|
||||
@value-changed=${this._iconChanged}
|
||||
.label=${this.hass.localize("ui.panel.config.category.editor.icon")}
|
||||
></ha-icon-picker>
|
||||
</div>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid || !!this._submitting}
|
||||
>
|
||||
${entry
|
||||
? this.hass.localize("ui.common.save")
|
||||
: this.hass.localize("ui.common.add")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid || !!this._submitting}
|
||||
>
|
||||
${entry
|
||||
? this.hass.localize("ui.common.save")
|
||||
: this.hass.localize("ui.common.add")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import { showCategoryRegistryDetailDialog } from "./show-dialog-category-registry-detail";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
const NO_CATEGORIES_ID = "___NO_CATEGORIES___";
|
||||
|
||||
@customElement("ha-category-picker")
|
||||
export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -101,17 +100,11 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _getCategories = memoizeOne(
|
||||
(categories: CategoryRegistryEntry[] | undefined): PickerComboBoxItem[] => {
|
||||
if (!categories || categories.length === 0) {
|
||||
return [
|
||||
{
|
||||
id: NO_CATEGORIES_ID,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.category-picker.no_categories"
|
||||
),
|
||||
icon_path: mdiTag,
|
||||
},
|
||||
];
|
||||
(
|
||||
categories: CategoryRegistryEntry[] | undefined
|
||||
): PickerComboBoxItem[] | undefined => {
|
||||
if (!categories) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const items = categories.map<PickerComboBoxItem>((category) => ({
|
||||
@@ -210,10 +203,6 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (value === NO_CATEGORIES_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
this._setValue(undefined);
|
||||
return;
|
||||
|
||||
@@ -33,10 +33,7 @@ import {
|
||||
checkForEntityUpdates,
|
||||
filterUpdateEntitiesParameterized,
|
||||
} from "../../../data/update";
|
||||
import {
|
||||
QuickBarMode,
|
||||
showQuickBar,
|
||||
} from "../../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
|
||||
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
@@ -375,7 +372,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
};
|
||||
|
||||
showQuickBar(this, {
|
||||
mode: QuickBarMode.Command,
|
||||
hint: this.hass.enableShortcuts
|
||||
? this.hass.localize("ui.dialogs.quick-bar.key_c_tip", params)
|
||||
: undefined,
|
||||
|
||||
@@ -51,7 +51,7 @@ export class HaDeviceInfoMatter extends SubscribeMixin(LitElement) {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.matter.device_info.device_info"
|
||||
"ui.panel.config.matter.device_info.matter_info"
|
||||
)}
|
||||
>
|
||||
<div class="row">
|
||||
|
||||
@@ -54,7 +54,7 @@ import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { createAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
import { getSubEntries, sortConfigEntries } from "../../../data/config_entries";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
|
||||
@@ -498,12 +498,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.entities.picker.headers.voice_assistants"
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
),
|
||||
type: "icon",
|
||||
type: "flex",
|
||||
defaultHidden: true,
|
||||
minWidth: "100px",
|
||||
maxWidth: "100px",
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
template: (entry) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entities,
|
||||
|
||||
@@ -227,7 +227,6 @@ export class DialogHelperDetail extends LitElement {
|
||||
src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
|
||||
@@ -487,12 +487,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.helpers.picker.headers.voice_assistants"
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
),
|
||||
type: "icon",
|
||||
type: "flex",
|
||||
defaultHidden: true,
|
||||
minWidth: "100px",
|
||||
maxWidth: "100px",
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
template: (helper) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
|
||||
@@ -60,7 +60,6 @@ class HaDomainIntegrations extends LitElement {
|
||||
src=${brandsUrl({
|
||||
domain: flow.handler,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
@@ -106,7 +105,6 @@ class HaDomainIntegrations extends LitElement {
|
||||
src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
@@ -170,7 +168,6 @@ class HaDomainIntegrations extends LitElement {
|
||||
src=${brandsUrl({
|
||||
domain: this.domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
|
||||
@@ -57,7 +57,6 @@ export class HaIntegrationListItem extends ListItemBase {
|
||||
src=${brandsUrl({
|
||||
domain: this.integration.domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
brand: this.brand,
|
||||
})}
|
||||
|
||||
@@ -23,23 +23,12 @@ import {
|
||||
subscribeBluetoothScannersDetails,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { bluetoothTabs } from "./bluetooth-config-dashboard";
|
||||
import { showBluetoothDeviceInfoDialog } from "./show-dialog-bluetooth-device-info";
|
||||
|
||||
export const bluetoothAdvertisementMonitorTabs: PageNavigation[] = [
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.advertisement_monitor",
|
||||
path: "advertisement-monitor",
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.visualization",
|
||||
path: "visualization",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("bluetooth-advertisement-monitor")
|
||||
export class BluetoothAdvertisementMonitorPanel extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -232,7 +221,7 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
filter=${this.address || ""}
|
||||
clickable
|
||||
.tabs=${bluetoothAdvertisementMonitorTabs}
|
||||
.tabs=${bluetoothTabs}
|
||||
></hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { mdiCogOutline } from "@mdi/js";
|
||||
import {
|
||||
mdiBroadcast,
|
||||
mdiCogOutline,
|
||||
mdiLan,
|
||||
mdiLinkVariant,
|
||||
mdiNetwork,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -8,7 +14,6 @@ import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-list";
|
||||
import "../../../../../components/ha-list-item";
|
||||
|
||||
import type {
|
||||
BluetoothAllocationsData,
|
||||
BluetoothScannerState,
|
||||
@@ -24,16 +29,44 @@ import type { ConfigEntry } from "../../../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../../../data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
|
||||
export const bluetoothTabs: PageNavigation[] = [
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.tabs.overview",
|
||||
path: `/config/bluetooth/dashboard`,
|
||||
iconPath: mdiNetwork,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.tabs.advertisements",
|
||||
path: `/config/bluetooth/advertisement-monitor`,
|
||||
iconPath: mdiBroadcast,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.tabs.visualization",
|
||||
path: `/config/bluetooth/visualization`,
|
||||
iconPath: mdiLan,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.tabs.connections",
|
||||
path: `/config/bluetooth/connection-monitor`,
|
||||
iconPath: mdiLinkVariant,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("bluetooth-config-dashboard")
|
||||
export class BluetoothConfigDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@state() private _configEntries: ConfigEntry[] = [];
|
||||
|
||||
@state() private _connectionAllocationData: BluetoothAllocationsData[] = [];
|
||||
@@ -122,10 +155,12 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
<hass-tabs-subpage
|
||||
header=${this.hass.localize("ui.panel.config.bluetooth.title")}
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
.tabs=${bluetoothTabs}
|
||||
>
|
||||
<div class="content">
|
||||
<ha-card
|
||||
@@ -135,60 +170,8 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
>
|
||||
<ha-list>${this._renderAdaptersList()}</ha-list>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.advertisement_monitor"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.advertisement_monitor_details"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
href="/config/bluetooth/advertisement-monitor"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.advertisement_monitor"
|
||||
)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
href="/config/bluetooth/visualization"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.bluetooth.visualization")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.connection_slot_allocations_monitor"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.connection_slot_allocations_monitor_description"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
href="/config/bluetooth/connection-monitor"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.connection_monitor"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import type { DeviceRegistryEntry } from "../../../../../data/device/device_regi
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { bluetoothTabs } from "./bluetooth-config-dashboard";
|
||||
|
||||
@customElement("bluetooth-connection-monitor")
|
||||
export class BluetoothConnectionMonitorPanel extends LitElement {
|
||||
@@ -214,6 +215,7 @@ export class BluetoothConnectionMonitorPanel extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${bluetoothTabs}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._dataWithNamedSourceAndIds(this._data)}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
|
||||
@@ -26,9 +26,9 @@ import {
|
||||
subscribeBluetoothScannersDetails,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { bluetoothAdvertisementMonitorTabs } from "./bluetooth-advertisement-monitor";
|
||||
import { bluetoothTabs } from "./bluetooth-config-dashboard";
|
||||
|
||||
const UPDATE_THROTTLE_TIME = 10000;
|
||||
|
||||
@@ -123,8 +123,7 @@ export class BluetoothNetworkVisualization extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
header=${this.hass.localize("ui.panel.config.bluetooth.visualization")}
|
||||
.tabs=${bluetoothAdvertisementMonitorTabs}
|
||||
.tabs=${bluetoothTabs}
|
||||
>
|
||||
<ha-network-graph
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -203,7 +203,6 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
src=${brandsUrl({
|
||||
domain: preview_feature.domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
|
||||
@@ -175,24 +175,31 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
template: narrow
|
||||
? undefined
|
||||
: (dashboard) => html`
|
||||
${dashboard.title}
|
||||
${dashboard.default
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.id="default-icon-${dashboard.title}"
|
||||
style="padding-left: 10px; padding-inline-start: 10px; padding-inline-end: initial; direction: var(--direction);"
|
||||
.path=${mdiHomeCircleOutline}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
.for="default-icon-${dashboard.title}"
|
||||
placement="right"
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.default_dashboard`
|
||||
)}
|
||||
</ha-tooltip>
|
||||
`
|
||||
: nothing}
|
||||
<span
|
||||
style="display:flex; align-items:center; gap: var(--ha-space-2); min-width:0; width:100%;"
|
||||
>
|
||||
<span
|
||||
style="min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1;"
|
||||
>${dashboard.title}</span
|
||||
>
|
||||
${dashboard.default
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.id="default-icon-${dashboard.title}"
|
||||
style="flex-shrink:0;"
|
||||
.path=${mdiHomeCircleOutline}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
.for="default-icon-${dashboard.title}"
|
||||
placement="right"
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.default_dashboard`
|
||||
)}
|
||||
</ha-tooltip>
|
||||
`
|
||||
: nothing}
|
||||
</span>
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -75,7 +75,6 @@ class HaConfigRepairs extends LitElement {
|
||||
src=${brandsUrl({
|
||||
domain: issue.issue_domain || issue.domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
.title=${domainName}
|
||||
|
||||
@@ -58,7 +58,6 @@ class IntegrationsStartupTime extends LitElement {
|
||||
src=${brandsUrl({
|
||||
domain: setup.domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
|
||||
@@ -61,7 +61,7 @@ import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { createAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { CategoryRegistryEntry } from "../../../data/category_registry";
|
||||
import {
|
||||
createCategoryRegistryEntry,
|
||||
@@ -415,12 +415,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.scene.picker.headers.voice_assistants"
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
),
|
||||
type: "icon",
|
||||
type: "flex",
|
||||
defaultHidden: true,
|
||||
minWidth: "100px",
|
||||
maxWidth: "100px",
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
template: (scene) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
@@ -1189,13 +1189,19 @@ ${rejected
|
||||
private async _duplicate(scene) {
|
||||
if (scene.attributes.id) {
|
||||
const config = await getSceneConfig(this.hass, scene.attributes.id);
|
||||
showSceneEditor({
|
||||
...config,
|
||||
id: undefined,
|
||||
name: `${config?.name} (${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.duplicate"
|
||||
)})`,
|
||||
});
|
||||
const entityRegEntry = this._entityReg.find(
|
||||
(reg) => reg.entity_id === scene.entity_id
|
||||
);
|
||||
showSceneEditor(
|
||||
{
|
||||
...config,
|
||||
id: undefined,
|
||||
name: `${config?.name} (${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.duplicate"
|
||||
)})`,
|
||||
},
|
||||
entityRegEntry?.area_id || undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { createAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { CategoryRegistryEntry } from "../../../data/category_registry";
|
||||
import {
|
||||
createCategoryRegistryEntry,
|
||||
@@ -403,12 +403,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.script.picker.headers.voice_assistants"
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
),
|
||||
type: "icon",
|
||||
type: "flex",
|
||||
defaultHidden: true,
|
||||
minWidth: "100px",
|
||||
maxWidth: "100px",
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
template: (script) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
|
||||
@@ -8,6 +8,8 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { computeEntityNameList } from "../../../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/search-input";
|
||||
import "../../../components/ha-dialog";
|
||||
@@ -18,10 +20,16 @@ import "../../../components/ha-list";
|
||||
import type { ExposeEntitySettings } from "../../../data/expose";
|
||||
import { voiceAssistants } from "../../../data/expose";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { loadVirtualizer } from "../../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./entity-voice-settings";
|
||||
import type { ExposeEntityDialogParams } from "./show-dialog-expose-entity";
|
||||
|
||||
interface FilteredEntity {
|
||||
entity: HassEntity;
|
||||
nameList: (string | undefined)[];
|
||||
}
|
||||
|
||||
@customElement("dialog-expose-entity")
|
||||
class DialogExposeEntity extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -32,6 +40,12 @@ class DialogExposeEntity extends LitElement {
|
||||
|
||||
@state() private _selected: string[] = [];
|
||||
|
||||
public willUpdate(): void {
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
}
|
||||
}
|
||||
|
||||
public async showDialog(params: ExposeEntityDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
}
|
||||
@@ -141,38 +155,101 @@ class DialogExposeEntity extends LitElement {
|
||||
(
|
||||
exposedEntities: Record<string, ExposeEntitySettings>,
|
||||
filter?: string
|
||||
) => {
|
||||
): FilteredEntity[] => {
|
||||
const lowerFilter = filter?.toLowerCase();
|
||||
return Object.values(this.hass.states).filter(
|
||||
(entity) =>
|
||||
this._params!.filterAssistants.some(
|
||||
(ass) => !exposedEntities[entity.entity_id]?.[ass]
|
||||
) &&
|
||||
(!lowerFilter ||
|
||||
entity.entity_id.toLowerCase().includes(lowerFilter) ||
|
||||
computeStateName(entity)?.toLowerCase().includes(lowerFilter))
|
||||
);
|
||||
const result: FilteredEntity[] = [];
|
||||
|
||||
for (const entity of Object.values(this.hass.states)) {
|
||||
if (
|
||||
this._params!.filterAssistants.every(
|
||||
(ass) => exposedEntities[entity.entity_id]?.[ass]
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nameList = computeEntityNameList(
|
||||
entity,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
if (!lowerFilter) {
|
||||
result.push({ entity, nameList });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entity.entity_id.toLowerCase().includes(lowerFilter)) {
|
||||
result.push({ entity, nameList });
|
||||
continue;
|
||||
}
|
||||
|
||||
const entityName = computeStateName(entity);
|
||||
if (entityName?.toLowerCase().includes(lowerFilter)) {
|
||||
result.push({ entity, nameList });
|
||||
continue;
|
||||
}
|
||||
|
||||
const [, deviceName, areaName] = nameList;
|
||||
|
||||
if (deviceName?.toLowerCase().includes(lowerFilter)) {
|
||||
result.push({ entity, nameList });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (areaName?.toLowerCase().includes(lowerFilter)) {
|
||||
result.push({ entity, nameList });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
|
||||
private _renderItem = (entityState: HassEntity) => html`
|
||||
<ha-check-list-item
|
||||
graphic="icon"
|
||||
twoLine
|
||||
.value=${entityState.entity_id}
|
||||
.selected=${this._selected.includes(entityState.entity_id)}
|
||||
@request-selected=${this._handleSelected}
|
||||
>
|
||||
<ha-state-icon
|
||||
title=${ifDefined(entityState?.state)}
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entityState}
|
||||
></ha-state-icon>
|
||||
${computeStateName(entityState)}
|
||||
<span slot="secondary">${entityState.entity_id}</span>
|
||||
</ha-check-list-item>
|
||||
`;
|
||||
private _renderItem = (item: FilteredEntity) => {
|
||||
const { entity: entityState, nameList } = item;
|
||||
const [entityName, deviceName, areaName] = nameList;
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const primary = entityName || deviceName || entityState.entity_id;
|
||||
const context = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
||||
|
||||
return html`
|
||||
<ha-check-list-item
|
||||
graphic="icon"
|
||||
?twoLine=${context}
|
||||
?threeLine=${showEntityId}
|
||||
.value=${entityState.entity_id}
|
||||
.selected=${this._selected.includes(entityState.entity_id)}
|
||||
@request-selected=${this._handleSelected}
|
||||
>
|
||||
<ha-state-icon
|
||||
title=${ifDefined(entityState?.state)}
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entityState}
|
||||
></ha-state-icon>
|
||||
${primary}
|
||||
${context || showEntityId
|
||||
? html`<span slot="secondary">
|
||||
${context}
|
||||
${showEntityId
|
||||
? html`<br /><span class="entity-id"
|
||||
>${entityState.entity_id}</span
|
||||
>`
|
||||
: nothing}
|
||||
</span>`
|
||||
: nothing}
|
||||
</ha-check-list-item>
|
||||
`;
|
||||
};
|
||||
|
||||
private _expose() {
|
||||
this._params!.exposeEntities(this._selected);
|
||||
@@ -198,6 +275,7 @@ class DialogExposeEntity extends LitElement {
|
||||
width: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
margin-top: var(--ha-space-2);
|
||||
--text-field-suffix-padding-left: 8px;
|
||||
}
|
||||
.header {
|
||||
@@ -210,7 +288,7 @@ class DialogExposeEntity extends LitElement {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: -4px 0;
|
||||
margin: calc(var(--ha-space-1) * -1) 0;
|
||||
}
|
||||
.subtitle {
|
||||
color: var(--secondary-text-color);
|
||||
@@ -225,9 +303,17 @@ class DialogExposeEntity extends LitElement {
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
}
|
||||
ha-check-list-item[threeLine] {
|
||||
height: 88px;
|
||||
}
|
||||
ha-check-list-item .entity-id {
|
||||
line-height: var(--ha-line-height-normal);
|
||||
padding-left: var(--ha-space-1);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
ha-check-list-item ha-state-icon {
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-left: var(--ha-space-6);
|
||||
margin-inline-start: var(--ha-space-6);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
@media all and (max-height: 800px) {
|
||||
@@ -262,8 +348,8 @@ class DialogExposeEntity extends LitElement {
|
||||
--text-field-suffix-padding-left: unset;
|
||||
}
|
||||
ha-check-list-item ha-state-icon {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,20 +167,21 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
template: narrow
|
||||
? undefined
|
||||
: (entry) => html`
|
||||
${entry.name}<br />
|
||||
<div class="secondary">${entry.entity_id}</div>
|
||||
`,
|
||||
},
|
||||
// For search & narrow
|
||||
area: {
|
||||
title: localize("ui.panel.config.voice_assistants.expose.headers.area"),
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
filterable: true,
|
||||
template: (entry) => entry.area || "—",
|
||||
},
|
||||
entity_id: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.entity_id"
|
||||
),
|
||||
hidden: !narrow,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
domain: {
|
||||
title: localize(
|
||||
@@ -191,13 +192,6 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.voice_assistants.expose.headers.area"),
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
filterable: true,
|
||||
template: (entry) => entry.area || "—",
|
||||
},
|
||||
assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
@@ -819,8 +813,8 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
}
|
||||
.selected-txt {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
padding-left: 16px;
|
||||
padding-inline-start: 16px;
|
||||
padding-left: var(--ha-space-4);
|
||||
padding-inline-start: var(--ha-space-4);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.table-header .selected-txt {
|
||||
@@ -830,8 +824,8 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
.header-toolbar .header-btns {
|
||||
margin-right: -12px;
|
||||
margin-inline-end: -12px;
|
||||
margin-right: calc(var(--ha-space-3) * -1);
|
||||
margin-inline-end: calc(var(--ha-space-3) * -1);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.header-btns {
|
||||
@@ -839,17 +833,17 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
}
|
||||
.header-btns > ha-button,
|
||||
.header-btns > ha-icon-button {
|
||||
margin: 8px;
|
||||
margin: var(--ha-space-2);
|
||||
}
|
||||
ha-button-menu {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.clear {
|
||||
color: var(--primary-color);
|
||||
padding-left: 8px;
|
||||
padding-inline-start: 8px;
|
||||
padding-left: var(--ha-space-2);
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
text-transform: uppercase;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
@@ -160,6 +160,10 @@ class PanelDeveloperTools extends LitElement {
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
transition:
|
||||
width var(--ha-animation-base-duration) ease,
|
||||
padding-left var(--ha-animation-base-duration) ease,
|
||||
padding-right var(--ha-animation-base-duration) ease;
|
||||
}
|
||||
:host([narrow]) .header {
|
||||
width: calc(
|
||||
@@ -170,6 +174,11 @@ class PanelDeveloperTools extends LitElement {
|
||||
);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.header {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
height: var(--header-height);
|
||||
|
||||
@@ -186,7 +186,11 @@ class PanelLight extends LitElement {
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
z-index: 4;
|
||||
transition: box-shadow 200ms linear;
|
||||
transition:
|
||||
box-shadow 200ms linear,
|
||||
width var(--ha-animation-base-duration) ease,
|
||||
padding-left var(--ha-animation-base-duration) ease,
|
||||
padding-right var(--ha-animation-base-duration) ease;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
@@ -211,6 +215,11 @@ class PanelLight extends LitElement {
|
||||
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
|
||||
);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.header {
|
||||
transition: box-shadow 200ms linear;
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
height: var(--header-height);
|
||||
display: flex;
|
||||
|
||||
@@ -160,7 +160,6 @@ class HaLogbookRenderer extends LitElement {
|
||||
? brandsUrl({
|
||||
domain: domain!,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
@@ -16,7 +16,7 @@ import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-domain-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { computeCssVariable } from "../../../resources/css-variables";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -2,8 +2,6 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { ClockCardConfig } from "../types";
|
||||
@@ -28,11 +26,6 @@ function romanize12HourClock(num: number) {
|
||||
return numerals[num];
|
||||
}
|
||||
|
||||
const INTERVAL = 1000;
|
||||
const QUARTER_TICKS = Array.from({ length: 4 }, (_, i) => i);
|
||||
const HOUR_TICKS = Array.from({ length: 12 }, (_, i) => i);
|
||||
const MINUTE_TICKS = Array.from({ length: 60 }, (_, i) => i);
|
||||
|
||||
@customElement("hui-clock-card-analog")
|
||||
export class HuiClockCardAnalog extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@@ -47,49 +40,6 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
|
||||
@state() private _secondOffsetSec?: number;
|
||||
|
||||
@state() private _year = "";
|
||||
|
||||
@state() private _month = "";
|
||||
|
||||
@state() private _day = "";
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
|
||||
private _currentDate = new Date();
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
this._computeDateTime();
|
||||
if (this.config?.date && this.config.date !== "none") {
|
||||
this._startTick();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
this._stopTick();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.locale !== this.hass?.locale) {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
this._computeDateTime();
|
||||
}
|
||||
};
|
||||
|
||||
private _initDate() {
|
||||
if (!this.config || !this.hass) {
|
||||
return;
|
||||
@@ -101,27 +51,6 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
}
|
||||
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
...(this.config.date && this.config.date !== "none"
|
||||
? this.config.date === "day"
|
||||
? {
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month"
|
||||
? {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month-long"
|
||||
? {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}
|
||||
: {
|
||||
year: "numeric",
|
||||
month: this.config.date === "long" ? "long" : "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: {}),
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
@@ -131,31 +60,42 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
|
||||
});
|
||||
|
||||
this._computeDateTime();
|
||||
this._computeOffsets();
|
||||
}
|
||||
|
||||
private _startTick() {
|
||||
this._tick();
|
||||
this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
|
||||
}
|
||||
|
||||
private _stopTick() {
|
||||
if (this._tickInterval) {
|
||||
clearInterval(this._tickInterval);
|
||||
this._tickInterval = undefined;
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.locale !== this.hass?.locale) {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateDate() {
|
||||
this._currentDate = new Date();
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
this._computeOffsets();
|
||||
}
|
||||
|
||||
private _computeDateTime() {
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
}
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
this._computeOffsets();
|
||||
}
|
||||
};
|
||||
|
||||
private _computeOffsets() {
|
||||
if (!this._dateTimeFormat) return;
|
||||
|
||||
this._updateDate();
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts(this._currentDate);
|
||||
const parts = this._dateTimeFormat.formatToParts();
|
||||
const hourStr = parts.find((p) => p.type === "hour")?.value;
|
||||
const minuteStr = parts.find((p) => p.type === "minute")?.value;
|
||||
const secondStr = parts.find((p) => p.type === "second")?.value;
|
||||
@@ -163,7 +103,7 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
const hour = hourStr ? parseInt(hourStr, 10) : 0;
|
||||
const minute = minuteStr ? parseInt(minuteStr, 10) : 0;
|
||||
const second = secondStr ? parseInt(secondStr, 10) : 0;
|
||||
const ms = this._currentDate.getMilliseconds();
|
||||
const ms = new Date().getMilliseconds();
|
||||
const secondsWithMs = second + ms / 1000;
|
||||
|
||||
const hour12 = hour % 12;
|
||||
@@ -171,38 +111,18 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
this._secondOffsetSec = secondsWithMs;
|
||||
this._minuteOffsetSec = minute * 60 + secondsWithMs;
|
||||
this._hourOffsetSec = hour12 * 3600 + minute * 60 + secondsWithMs;
|
||||
|
||||
// Also update date parts if date is shown
|
||||
if (this.config?.date && this.config.date !== "none") {
|
||||
this._year = parts.find((p) => p.type === "year")?.value ?? "";
|
||||
this._month = parts.find((p) => p.type === "month")?.value ?? "";
|
||||
this._day = parts.find((p) => p.type === "day")?.value ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
private _tick() {
|
||||
this._computeDateTime();
|
||||
}
|
||||
|
||||
private _computeClock = memoizeOne((config: ClockCardConfig) => {
|
||||
const faceParts = config.face_style?.split("_");
|
||||
const isLongDate =
|
||||
config.date === "day-month-long" || config.date === "long";
|
||||
|
||||
return {
|
||||
sizeClass: config.clock_size ? `size-${config.clock_size}` : "",
|
||||
isNumbers: faceParts?.includes("numbers") ?? false,
|
||||
isRoman: faceParts?.includes("roman") ?? false,
|
||||
isUpright: faceParts?.includes("upright") ?? false,
|
||||
isLongDate,
|
||||
};
|
||||
});
|
||||
|
||||
render() {
|
||||
if (!this.config) return nothing;
|
||||
|
||||
const { sizeClass, isNumbers, isRoman, isUpright, isLongDate } =
|
||||
this._computeClock(this.config);
|
||||
const sizeClass = this.config.clock_size
|
||||
? `size-${this.config.clock_size}`
|
||||
: "";
|
||||
|
||||
const isNumbers = this.config?.face_style?.startsWith("numbers");
|
||||
const isRoman = this.config?.face_style?.startsWith("roman");
|
||||
const isUpright = this.config?.face_style?.endsWith("upright");
|
||||
|
||||
const indicator = (number?: number) => html`
|
||||
<div
|
||||
@@ -243,14 +163,14 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
})}
|
||||
>
|
||||
${this.config.ticks === "quarter"
|
||||
? QUARTER_TICKS.map(
|
||||
? Array.from({ length: 4 }, (_, i) => i).map(
|
||||
(i) =>
|
||||
// 4 ticks (12, 3, 6, 9) at 0°, 90°, 180°, 270°
|
||||
html`
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="tick hour"
|
||||
style=${styleMap({ "--tick-rotation": `${i * 90}deg` })}
|
||||
style=${`--tick-rotation: ${i * 90}deg;`}
|
||||
>
|
||||
${indicator([12, 3, 6, 9][i])}
|
||||
</div>
|
||||
@@ -258,30 +178,28 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
)
|
||||
: !this.config.ticks || // Default to hour ticks
|
||||
this.config.ticks === "hour"
|
||||
? HOUR_TICKS.map(
|
||||
? Array.from({ length: 12 }, (_, i) => i).map(
|
||||
(i) =>
|
||||
// 12 ticks (1-12)
|
||||
html`
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="tick hour"
|
||||
style=${styleMap({ "--tick-rotation": `${i * 30}deg` })}
|
||||
style=${`--tick-rotation: ${i * 30}deg;`}
|
||||
>
|
||||
${indicator(((i + 11) % 12) + 1)}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
: this.config.ticks === "minute"
|
||||
? MINUTE_TICKS.map(
|
||||
? Array.from({ length: 60 }, (_, i) => i).map(
|
||||
(i) =>
|
||||
// 60 ticks (1-60)
|
||||
html`
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="tick ${i % 5 === 0 ? "hour" : "minute"}"
|
||||
style=${styleMap({
|
||||
"--tick-rotation": `${i * 6}deg`,
|
||||
})}
|
||||
style=${`--tick-rotation: ${i * 6}deg;`}
|
||||
>
|
||||
${i % 5 === 0
|
||||
? indicator(((i / 5 + 11) % 12) + 1)
|
||||
@@ -290,33 +208,14 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
${this.config?.date && this.config.date !== "none"
|
||||
? html`<div
|
||||
class=${classMap({
|
||||
"date-parts": true,
|
||||
[sizeClass]: true,
|
||||
"long-date": isLongDate,
|
||||
})}
|
||||
>
|
||||
<span class="date-part day-month"
|
||||
>${this._day} ${this._month}</span
|
||||
>
|
||||
<span class="date-part year">${this._year}</span>
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
<div class="center-dot"></div>
|
||||
<div
|
||||
class="hand hour"
|
||||
style=${styleMap({
|
||||
"animation-delay": `-${this._hourOffsetSec ?? 0}s`,
|
||||
})}
|
||||
style=${`animation-delay: -${this._hourOffsetSec ?? 0}s;`}
|
||||
></div>
|
||||
<div
|
||||
class="hand minute"
|
||||
style=${styleMap({
|
||||
"animation-delay": `-${this._minuteOffsetSec ?? 0}s`,
|
||||
})}
|
||||
style=${`animation-delay: -${this._minuteOffsetSec ?? 0}s;`}
|
||||
></div>
|
||||
${this.config.show_seconds
|
||||
? html`<div
|
||||
@@ -325,13 +224,11 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
second: true,
|
||||
step: this.config.seconds_motion === "tick",
|
||||
})}
|
||||
style=${styleMap({
|
||||
"animation-delay": `-${
|
||||
(this.config.seconds_motion === "tick"
|
||||
? Math.floor(this._secondOffsetSec ?? 0)
|
||||
: (this._secondOffsetSec ?? 0)) as number
|
||||
}s`,
|
||||
})}
|
||||
style=${`animation-delay: -${
|
||||
(this.config.seconds_motion === "tick"
|
||||
? Math.floor(this._secondOffsetSec ?? 0)
|
||||
: (this._secondOffsetSec ?? 0)) as number
|
||||
}s;`}
|
||||
></div>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -373,14 +270,6 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Modern browsers: Use container queries for responsive font sizing */
|
||||
@supports (container-type: inline-size) {
|
||||
.dial {
|
||||
container-type: inline-size;
|
||||
container-name: clock;
|
||||
}
|
||||
}
|
||||
|
||||
.dial-border {
|
||||
border: 2px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
@@ -518,78 +407,6 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
transform: translate(-50%, 0) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.date-parts {
|
||||
position: absolute;
|
||||
top: 68%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-areas:
|
||||
"day-month"
|
||||
"year";
|
||||
direction: ltr;
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
max-width: 87%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Modern browsers: Use container queries for responsive font sizing */
|
||||
@supports (container-type: inline-size) {
|
||||
/* Small clock with long date: reduce to xs */
|
||||
@container clock (max-width: 139px) {
|
||||
.date-parts.long-date {
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
}
|
||||
|
||||
/* Medium clock: scale up */
|
||||
@container clock (min-width: 140px) {
|
||||
.date-parts {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
}
|
||||
|
||||
/* Large clock: scale up more */
|
||||
@container clock (min-width: 200px) {
|
||||
.date-parts {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Legacy browsers: Use existing size classes */
|
||||
@supports not (container-type: inline-size) {
|
||||
/* Small clock (no size class) with long date */
|
||||
.date-parts.long-date:not(.size-medium):not(.size-large) {
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
|
||||
.date-parts.size-medium {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.date-parts.size-large {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
}
|
||||
}
|
||||
|
||||
.date-part.day-month {
|
||||
grid-area: day-month;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.date-part.year {
|
||||
grid-area: year;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
|
||||
@state() private _timeAmPm?: string;
|
||||
|
||||
@state() private _date?: string;
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
|
||||
private _initDate() {
|
||||
@@ -41,27 +39,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
|
||||
const h12 = useAmPm(locale);
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
...(this.config.date && this.config.date !== "none"
|
||||
? this.config.date === "day"
|
||||
? {
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month"
|
||||
? {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month-long"
|
||||
? {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}
|
||||
: {
|
||||
year: "numeric",
|
||||
month: this.config.date === "long" ? "long" : "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: {}),
|
||||
hour: h12 ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
@@ -116,16 +93,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
? parts.find((part) => part.type === "second")?.value
|
||||
: undefined;
|
||||
this._timeAmPm = parts.find((part) => part.type === "dayPeriod")?.value;
|
||||
|
||||
this._date = this.config?.date
|
||||
? [
|
||||
parts.find((part) => part.type === "day")?.value,
|
||||
parts.find((part) => part.type === "month")?.value,
|
||||
parts.find((part) => part.type === "year")?.value,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
: undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -146,9 +113,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
? html`<div class="time-part am-pm">${this._timeAmPm}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.config.date && this.config.date !== "none"
|
||||
? html`<div class="date ${sizeClass}">${this._date}</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -224,20 +188,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
content: ":";
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.date {
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
|
||||
.date.size-medium {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.date.size-large {
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { EnergyData, EnergyPreferences } from "../../../../data/energy";
|
||||
import {
|
||||
formatPowerShort,
|
||||
@@ -326,6 +327,7 @@ class HuiPowerSankeyCard
|
||||
color: getGraphColorByIndex(idx, computedStyle),
|
||||
index: 4,
|
||||
parent: effectiveParent,
|
||||
entityId: device.stat_rate,
|
||||
};
|
||||
if (node.parent) {
|
||||
parentLinks[node.id] = node.parent;
|
||||
@@ -461,6 +463,7 @@ class HuiPowerSankeyCard
|
||||
.data=${{ nodes, links }}
|
||||
.vertical=${vertical}
|
||||
.valueFormatter=${this._valueFormatter}
|
||||
@node-click=${this._handleNodeClick}
|
||||
></ha-sankey-chart>`
|
||||
: html`${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.no_data"
|
||||
@@ -475,6 +478,13 @@ class HuiPowerSankeyCard
|
||||
${formatPowerShort(this.hass, value)}
|
||||
</div>`;
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
if (node.entityId) {
|
||||
fireEvent(this, "hass-more-info", { entityId: node.entityId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute real-time power data from current entity states.
|
||||
* Similar to computeConsumptionData but for instantaneous power.
|
||||
|
||||
@@ -73,13 +73,18 @@ export class HuiCard extends ConditionalListenerMixin<LovelaceCardConfig>(
|
||||
};
|
||||
|
||||
// If the element has fixed rows or columns, we use the values from the element
|
||||
// unless the user has already configured their own
|
||||
if (elementOptions.fixed_rows) {
|
||||
mergedConfig.rows = elementOptions.rows;
|
||||
if (configOptions.rows === undefined) {
|
||||
mergedConfig.rows = elementOptions.rows;
|
||||
}
|
||||
delete mergedConfig.min_rows;
|
||||
delete mergedConfig.max_rows;
|
||||
}
|
||||
if (elementOptions.fixed_columns) {
|
||||
mergedConfig.columns = elementOptions.columns;
|
||||
if (configOptions.columns === undefined) {
|
||||
mergedConfig.columns = elementOptions.columns;
|
||||
}
|
||||
delete mergedConfig.min_columns;
|
||||
delete mergedConfig.max_columns;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ import { findEntities } from "../common/find-entities";
|
||||
import type { LovelaceElement, LovelaceElementConfig } from "../elements/types";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element";
|
||||
import type { PictureElementsCardConfig } from "./types";
|
||||
import {
|
||||
PREVIEW_CLICK_CALLBACK,
|
||||
type PictureElementsCardConfig,
|
||||
} from "./types";
|
||||
import type { PersonEntity } from "../../../data/person";
|
||||
|
||||
@customElement("hui-picture-elements-card")
|
||||
@@ -166,6 +169,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
.aspectRatio=${this._config.aspect_ratio}
|
||||
.darkModeFilter=${this._config.dark_mode_filter}
|
||||
.darkModeImage=${darkModeImage}
|
||||
@click=${this._handleImageClick}
|
||||
></hui-image>
|
||||
${this._elements}
|
||||
</div>
|
||||
@@ -221,6 +225,19 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
curCardEl === elToReplace ? newCardEl : curCardEl
|
||||
);
|
||||
}
|
||||
|
||||
private _handleImageClick(ev: MouseEvent): void {
|
||||
if (!this.preview || !this._config?.[PREVIEW_CLICK_CALLBACK]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
const x = ((ev.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((ev.clientY - rect.top) / rect.height) * 100;
|
||||
|
||||
// only the edited card has this callback
|
||||
this._config[PREVIEW_CLICK_CALLBACK](x, y);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -421,7 +421,6 @@ export interface ClockCardConfig extends LovelaceCardConfig {
|
||||
time_format?: TimeFormat;
|
||||
time_zone?: string;
|
||||
no_background?: boolean;
|
||||
date?: "none" | "short" | "long" | "day" | "day-month" | "day-month-long";
|
||||
// Analog clock options
|
||||
border?: boolean;
|
||||
ticks?: "none" | "quarter" | "hour" | "minute";
|
||||
@@ -488,6 +487,10 @@ export interface PictureCardConfig extends LovelaceCardConfig {
|
||||
alt_text?: string;
|
||||
}
|
||||
|
||||
// Symbol for preview click callback - preserved through spreads, not serialized
|
||||
// This allows the editor to attach a callback that only exists on the edited card's config
|
||||
export const PREVIEW_CLICK_CALLBACK = Symbol("previewClickCallback");
|
||||
|
||||
export interface PictureElementsCardConfig extends LovelaceCardConfig {
|
||||
title?: string;
|
||||
image?: string | MediaSelectorValue;
|
||||
@@ -502,6 +505,7 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig {
|
||||
theme?: string;
|
||||
dark_mode_image?: string | MediaSelectorValue;
|
||||
dark_mode_filter?: string;
|
||||
[PREVIEW_CLICK_CALLBACK]?: (x: number, y: number) => void;
|
||||
}
|
||||
|
||||
export interface PictureEntityCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -39,19 +39,6 @@ const cardConfigStruct = assign(
|
||||
time_zone: optional(enums(Object.keys(timezones))),
|
||||
show_seconds: optional(boolean()),
|
||||
no_background: optional(boolean()),
|
||||
date: optional(
|
||||
defaulted(
|
||||
union([
|
||||
literal("none"),
|
||||
literal("short"),
|
||||
literal("long"),
|
||||
literal("day"),
|
||||
literal("day-month"),
|
||||
literal("day-month-long"),
|
||||
]),
|
||||
literal("none")
|
||||
)
|
||||
),
|
||||
// Analog clock options
|
||||
border: optional(defaulted(boolean(), false)),
|
||||
ticks: optional(
|
||||
@@ -106,7 +93,7 @@ export class HuiClockCardEditor
|
||||
name: "clock_style",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
mode: "dropdown",
|
||||
options: ["digital", "analog"].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
@@ -132,27 +119,6 @@ export class HuiClockCardEditor
|
||||
},
|
||||
{ name: "show_seconds", selector: { boolean: {} } },
|
||||
{ name: "no_background", selector: { boolean: {} } },
|
||||
{
|
||||
name: "date",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: [
|
||||
"none",
|
||||
"short",
|
||||
"long",
|
||||
"day",
|
||||
"day-month",
|
||||
"day-month-long",
|
||||
].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.dates.${value}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
...(clockStyle === "digital"
|
||||
? ([
|
||||
{
|
||||
@@ -294,14 +260,13 @@ export class HuiClockCardEditor
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _data = memoizeOne((config: ClockCardConfig) => ({
|
||||
private _data = memoizeOne((config) => ({
|
||||
clock_style: "digital",
|
||||
clock_size: "small",
|
||||
time_zone: "auto",
|
||||
time_format: "auto",
|
||||
show_seconds: false,
|
||||
no_background: false,
|
||||
date: "none",
|
||||
// Analog clock options
|
||||
border: false,
|
||||
ticks: "hour",
|
||||
@@ -325,9 +290,8 @@ export class HuiClockCardEditor
|
||||
.data=${this._data(this._config)}
|
||||
.schema=${this._schema(
|
||||
this.hass.localize,
|
||||
this._data(this._config)
|
||||
.clock_style as ClockCardConfig["clock_style"],
|
||||
this._data(this._config).ticks as ClockCardConfig["ticks"],
|
||||
this._data(this._config).clock_style,
|
||||
this._data(this._config).ticks,
|
||||
this._data(this._config).show_seconds
|
||||
)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@@ -403,10 +367,6 @@ export class HuiClockCardEditor
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.no_background`
|
||||
);
|
||||
case "date":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.date.label`
|
||||
);
|
||||
case "border":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.border.label`
|
||||
@@ -432,10 +392,6 @@ export class HuiClockCardEditor
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "date":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.date.description`
|
||||
);
|
||||
case "border":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.border.description`
|
||||
|
||||
@@ -15,12 +15,16 @@ import {
|
||||
} from "superstruct";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-switch";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { PictureElementsCardConfig } from "../../cards/types";
|
||||
import {
|
||||
PREVIEW_CLICK_CALLBACK,
|
||||
type PictureElementsCardConfig,
|
||||
} from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import "../hui-sub-element-editor";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
@@ -28,7 +32,6 @@ import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import "../hui-picture-elements-card-row-editor";
|
||||
import type { LovelaceElementConfig } from "../../elements/types";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
|
||||
const genericElementConfigStruct = type({
|
||||
@@ -66,6 +69,44 @@ export class HuiPictureElementsCardEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _onPreviewClick = (x: number, y: number): void => {
|
||||
if (this._subElementEditorConfig?.type === "element") {
|
||||
this._handlePositionClick(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
private _handlePositionClick(x: number, y: number): void {
|
||||
if (
|
||||
!this._subElementEditorConfig?.elementConfig ||
|
||||
this._subElementEditorConfig.type !== "element" ||
|
||||
this._subElementEditorConfig.elementConfig.type === "conditional"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elementConfig = this._subElementEditorConfig
|
||||
.elementConfig as LovelaceElementConfig;
|
||||
const currentPosition = (elementConfig.style as Record<string, string>)
|
||||
?.position;
|
||||
if (currentPosition && currentPosition !== "absolute") {
|
||||
return;
|
||||
}
|
||||
|
||||
const newElement = {
|
||||
...elementConfig,
|
||||
style: {
|
||||
...((elementConfig.style as Record<string, string>) || {}),
|
||||
left: `${Math.round(x)}%`,
|
||||
top: `${Math.round(y)}%`,
|
||||
},
|
||||
};
|
||||
|
||||
const updateEvent = new CustomEvent("config-changed", {
|
||||
detail: { config: newElement },
|
||||
});
|
||||
this._handleSubElementChanged(updateEvent);
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
@@ -138,6 +179,16 @@ export class HuiPictureElementsCardEditor
|
||||
|
||||
if (this._subElementEditorConfig) {
|
||||
return html`
|
||||
${this._subElementEditorConfig.type === "element" &&
|
||||
this._subElementEditorConfig.elementConfig?.type !== "conditional"
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.picture-elements.position_hint"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
<hui-sub-element-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._subElementEditorConfig}
|
||||
@@ -181,6 +232,7 @@ export class HuiPictureElementsCardEditor
|
||||
return;
|
||||
}
|
||||
|
||||
// no need to attach the preview click callback here, no element is being edited
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
@@ -191,7 +243,8 @@ export class HuiPictureElementsCardEditor
|
||||
const config = {
|
||||
...this._config,
|
||||
elements: ev.detail.elements as LovelaceElementConfig[],
|
||||
} as LovelaceCardConfig;
|
||||
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
|
||||
} as PictureElementsCardConfig;
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
|
||||
@@ -232,7 +285,12 @@ export class HuiPictureElementsCardEditor
|
||||
elementConfig: value,
|
||||
};
|
||||
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
fireEvent(this, "config-changed", {
|
||||
config: {
|
||||
...this._config,
|
||||
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user