Compare commits

..

1 Commits

Author SHA1 Message Date
Petar Petrov
b24ef59718 Include the area when duplicating a scene from the scene dashboard 2026-01-12 22:37:06 +02:00
26 changed files with 129 additions and 372 deletions

2
.gitignore vendored
View File

@@ -15,7 +15,7 @@ dist/
!.yarn/sdks
!.yarn/versions
.pnp.*
node_modules/
/node_modules/
yarn-error.log
npm-debug.log

2
.nvmrc
View File

@@ -1 +1 @@
24.13.0
24.12.0

View File

@@ -216,7 +216,7 @@
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.52.0",
"vite-tsconfig-paths": "6.0.4",
"vite-tsconfig-paths": "6.0.3",
"vitest": "4.0.16",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
@@ -236,6 +236,6 @@
},
"packageManager": "yarn@4.12.0",
"volta": {
"node": "24.13.0"
"node": "24.12.0"
}
}

View File

@@ -1,16 +1,6 @@
// From https://github.com/epoberezkin/fast-deep-equal
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
interface DeepEqualOptions {
/** Compare Symbol properties in addition to string keys */
compareSymbols?: boolean;
}
export const deepEqual = (
a: any,
b: any,
options?: DeepEqualOptions
): boolean => {
export const deepEqual = (a: any, b: any): boolean => {
if (a === b) {
return true;
}
@@ -28,7 +18,7 @@ export const deepEqual = (
return false;
}
for (i = length; i-- !== 0; ) {
if (!deepEqual(a[i], b[i], options)) {
if (!deepEqual(a[i], b[i])) {
return false;
}
}
@@ -45,7 +35,7 @@ export const deepEqual = (
}
}
for (i of a.entries()) {
if (!deepEqual(i[1], b.get(i[0]), options)) {
if (!deepEqual(i[1], b.get(i[0]))) {
return false;
}
}
@@ -103,28 +93,11 @@ export const deepEqual = (
for (i = length; i-- !== 0; ) {
const key = keys[i];
if (!deepEqual(a[key], b[key], options)) {
if (!deepEqual(a[key], b[key])) {
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;
}

View File

@@ -1364,9 +1364,6 @@ export class HaDataTable extends LitElement {
.mdc-data-table__header-cell > * {
transition: var(--float-start) 0.2s ease;
}
.mdc-data-table__header-cell--numeric > span {
transition: none;
}
.mdc-data-table__header-cell ha-svg-icon {
top: -3px;
position: absolute;

View File

@@ -255,7 +255,6 @@ 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,

View File

@@ -14,6 +14,7 @@ import "./ha-icon-button";
import "./ha-label";
import "./ha-list";
import "./ha-list-item";
import "./search-input-outlined";
import "./voice-assistant-brand-icon";
import { voiceAssistants } from "../data/expose";
import "../panels/config/voice-assistants/expose/expose-assistant-icon";

View File

@@ -52,6 +52,8 @@ import "./ha-spinner";
import "./ha-svg-icon";
import "./user/ha-user-badge";
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
const SORT_VALUE_URL_PATHS = {
energy: 1,
map: 2,
@@ -342,6 +344,17 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
this._calculateCounts();
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
}
if (oldHass?.panelUrl !== this.hass.panelUrl) {
const selectedEl = this.shadowRoot!.querySelector(".selected");
if (selectedEl) {
// @ts-ignore
selectedEl.scrollIntoViewIfNeeded();
}
}
}
private _calculateCounts = throttle(() => {

View File

@@ -377,7 +377,7 @@ interface SelectBoxOptionImage {
}
export interface SelectOption {
value: string;
value: any;
label: string;
description?: string;
image?: string | SelectBoxOptionImage;

View File

@@ -1504,7 +1504,14 @@ 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);
z-index: 2;
}
@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);
}
}
@media all and (max-width: 870px), all and (max-height: 500px) {

View File

@@ -57,7 +57,6 @@ import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu";
@@ -661,15 +660,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-categories>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-voice-assistants>
<ha-filter-blueprints
.hass=${this.hass}
.type=${"automation"}
@@ -1040,7 +1030,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(categoryItems)
: new Set([...items].filter((x) => categoryItems!.has(x)));
} else if (
}
if (
key === "ha-filter-labels" &&
Array.isArray(filter.value) &&
filter.value.length
@@ -1062,29 +1053,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(labelItems)
: new Set([...items].filter((x) => labelItems!.has(x)));
} else if (
key === "ha-filter-voice-assistants" &&
Array.isArray(filter.value) &&
filter.value.length
) {
const assistItems = new Set<string>();
this.automations
.filter((automation) =>
getEntityVoiceAssistantsIds(
this._entityReg,
automation.entity_id
).some((va) => (filter.value as string[]).includes(va))
)
.forEach((automation) => assistItems.add(automation.entity_id));
if (!items) {
items = assistItems;
continue;
}
items =
"intersection" in items
? // @ts-ignore
items.intersection(assistItems)
: new Set([...items].filter((x) => assistItems!.has(x)));
}
}
this._filteredAutomations = items ? [...items] : undefined;

View File

@@ -206,8 +206,8 @@ class HaBlueprintOverview extends LitElement {
sortable: true,
valueColumn: "usageCount",
type: "numeric",
minWidth: "90px",
maxWidth: "90px",
minWidth: "100px",
maxWidth: "120px",
template: (blueprint) => {
const count = blueprint.usageCount ?? 0;
return html`

View File

@@ -101,13 +101,7 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
);
private _getCategories = memoizeOne(
(
categories: CategoryRegistryEntry[] | undefined
): PickerComboBoxItem[] | undefined => {
if (!categories) {
return undefined;
}
(categories: CategoryRegistryEntry[] | undefined): PickerComboBoxItem[] => {
if (!categories || categories.length === 0) {
return [
{

View File

@@ -1,16 +1,19 @@
import {
mdiDotsVertical,
mdiLocationEnter,
mdiLocationExit,
mdiRefresh,
} from "@mdi/js";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-alert";
import "../../../components/ha-bar";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-check-list-item";
import "../../../components/ha-list-item";
import "../../../components/ha-metric";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import type {
HassioSupervisorInfo,
@@ -30,9 +33,6 @@ import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types";
import "../dashboard/ha-config-updates";
import { showJoinBetaDialog } from "./updates/show-dialog-join-beta";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "@home-assistant/webawesome/dist/components/divider/divider";
@customElement("ha-config-section-updates")
class HaConfigSectionUpdates extends LitElement {
@@ -77,45 +77,35 @@ class HaConfigSectionUpdates extends LitElement {
.path=${mdiRefresh}
@click=${this._checkUpdates}
></ha-icon-button>
<ha-dropdown @wa-select=${this._handleOverflowAction}>
<ha-button-menu multi>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item
type="checkbox"
.checked=${this._showSkipped}
value="show_skipped"
<ha-check-list-item
left
@request-selected=${this._toggleSkipped}
.selected=${this._showSkipped}
>
${this.hass.localize("ui.panel.config.updates.show_skipped")}
</ha-dropdown-item>
</ha-check-list-item>
${this._supervisorInfo
? html`
<wa-divider></wa-divider>
<ha-dropdown-item
value="toggle_beta"
<li divider role="separator"></li>
<ha-list-item
@request-selected=${this._toggleBeta}
.disabled=${this._supervisorInfo.channel === "dev"}
>
<ha-svg-icon
.path=${this._supervisorInfo.channel === "stable"
? mdiLocationEnter
: mdiLocationExit}
slot="icon"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.updates.${this._supervisorInfo.channel === "stable" ? "join" : "leave"}_beta`
)}
${this._supervisorInfo.channel === "stable"
? this.hass.localize("ui.panel.config.updates.join_beta")
: this.hass.localize(
"ui.panel.config.updates.leave_beta"
)}
</ha-dropdown-item>
</ha-list-item>
`
: nothing}
</ha-dropdown>
</ha-button-menu>
</div>
<div class="content">
${canInstallUpdates.length
@@ -166,19 +156,27 @@ class HaConfigSectionUpdates extends LitElement {
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
}
private _handleOverflowAction(
ev: CustomEvent<{ item: { value: string } }>
): void {
if (ev.detail.item.value === "toggle_beta") {
if (this._supervisorInfo!.channel === "stable") {
showJoinBetaDialog(this, {
join: () => this._setChannel("beta"),
});
} else {
this._setChannel("stable");
}
} else if (ev.detail.item.value === "show_skipped") {
this._showSkipped = !this._showSkipped;
private _toggleSkipped(ev: CustomEvent<RequestSelectedDetail>): void {
if (ev.detail.source !== "property") {
return;
}
this._showSkipped = !this._showSkipped;
}
private async _toggleBeta(
ev: CustomEvent<RequestSelectedDetail>
): Promise<void> {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
if (this._supervisorInfo!.channel === "stable") {
showJoinBetaDialog(this, {
join: async () => this._setChannel("beta"),
});
} else {
this._setChannel("stable");
}
}

View File

@@ -51,7 +51,6 @@ import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
@@ -208,7 +207,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
})
private _activeHiddenColumns?: string[];
@state() private _helperEntities: HassEntity[] = [];
@state() private _stateItems: HassEntity[] = [];
@state() private _disabledEntityEntries?: EntityRegistryEntry[];
@@ -250,7 +249,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state() private _filteredHelperEntityIds?: string[] | null;
@state() private _filteredStateItems?: string[] | null;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
@@ -641,7 +640,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
protected render(): TemplateResult {
if (
!this.hass ||
this._helperEntities === undefined ||
this._stateItems === undefined ||
this._entityEntries === undefined ||
this._configEntries === undefined
) {
@@ -716,14 +715,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
const helpers = this._getItems(
this.hass.localize,
this._helperEntities,
this._stateItems,
this._disabledEntityEntries || [],
this._entityEntries,
this._configEntries,
this._entityReg,
this._categories,
this._labels,
this._filteredHelperEntityIds
this._filteredStateItems
);
return html`
<hass-tabs-subpage-data-table
@@ -810,15 +809,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-categories>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-voice-assistants>
${!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
@@ -981,7 +971,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
filter.length
) {
const labelItems = new Set<string>();
this._helperEntities
this._stateItems
.filter((stateItem) =>
entityRegistryByEntityId(this._entityReg)[
stateItem.entity_id
@@ -1000,13 +990,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(labelItems)
: new Set([...items].filter((x) => labelItems!.has(x)));
} else if (
}
if (
key === "ha-filter-categories" &&
Array.isArray(filter) &&
filter.length
) {
const categoryItems = new Set<string>();
this._helperEntities
this._stateItems
.filter(
(stateItem) =>
filter[0] ===
@@ -1026,39 +1017,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(categoryItems)
: new Set([...items].filter((x) => categoryItems!.has(x)));
} else if (
key === "ha-filter-voice-assistants" &&
Array.isArray(filter) &&
filter.length
) {
const assistItems = new Set<string>();
this._helperEntities
.filter((stateItem) =>
getEntityVoiceAssistantsIds(
this._entityReg,
stateItem.entity_id
).some((va) => (filter as string[]).includes(va))
)
.forEach((stateItem) => assistItems.add(stateItem.entity_id));
(this._disabledEntityEntries || [])
.filter((entry) =>
getEntityVoiceAssistantsIds(this._entityReg, entry.entity_id).some(
(va) => (filter as string[]).includes(va)
)
)
.forEach((entry) => assistItems.add(entry.entity_id));
if (!items) {
items = assistItems;
continue;
}
items =
"intersection" in items
? // @ts-ignore
items.intersection(assistItems)
: new Set([...items].filter((x) => assistItems!.has(x)));
}
}
this._filteredHelperEntityIds = items ? [...items] : undefined;
this._filteredStateItems = items ? [...items] : undefined;
}
public connectedCallback() {
@@ -1091,7 +1053,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
const device = this._searchParms.get("device");
const label = this._searchParms.get("label");
const category = this._searchParms.get("category");
const voiceAssistant = this._searchParms.get("voice_assistant");
if (!category && !label && !device) {
return;
@@ -1103,7 +1064,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
"ha-filter-devices": device ? [device] : [],
"ha-filter-labels": label ? [label] : [],
"ha-filter-categories": category ? [category] : [],
"ha-filter-voice-assistants": voiceAssistant ? [voiceAssistant] : [],
};
}
@@ -1343,7 +1303,7 @@ ${rejected
}
let changed =
!this._helperEntities ||
!this._stateItems ||
changedProps.has("_entityEntries") ||
changedProps.has("_configEntries") ||
changedProps.has("_entitySource");
@@ -1358,17 +1318,17 @@ ${rejected
const entityIds = Object.keys(this._entitySource);
const newHelpers = Object.values(this.hass!.states).filter(
const newStates = Object.values(this.hass!.states).filter(
(entity) =>
entityIds.includes(entity.entity_id) ||
isHelperDomain(computeStateDomain(entity))
);
if (
this._helperEntities.length !== newHelpers.length ||
!this._helperEntities.every((val, idx) => newHelpers[idx] === val)
this._stateItems.length !== newStates.length ||
!this._stateItems.every((val, idx) => newStates[idx] === val)
) {
this._helperEntities = newHelpers;
this._stateItems = newStates;
}
}

View File

@@ -51,7 +51,6 @@ import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
@@ -680,15 +679,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-categories>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-voice-assistants>
${!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
@@ -924,7 +914,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(categoryItems)
: new Set([...items].filter((x) => categoryItems!.has(x)));
} else if (
}
if (
key === "ha-filter-labels" &&
Array.isArray(filter.value) &&
filter.value.length
@@ -946,28 +937,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(labelItems)
: new Set([...items].filter((x) => labelItems!.has(x)));
} else if (
key === "ha-filter-voice-assistants" &&
Array.isArray(filter.value) &&
filter.value.length
) {
const assistItems = new Set<string>();
this.scenes
.filter((scene) =>
getEntityVoiceAssistantsIds(this._entityReg, scene.entity_id).some(
(va) => (filter.value as string[]).includes(va)
)
)
.forEach((scene) => assistItems.add(scene.entity_id));
if (!items) {
items = assistItems;
continue;
}
items =
"intersection" in items
? // @ts-ignore
items.intersection(assistItems)
: new Set([...items].filter((x) => assistItems!.has(x)));
}
}
this._filteredScenes = items ? [...items] : undefined;
@@ -1189,13 +1158,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
);
}
}

View File

@@ -53,7 +53,6 @@ import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
@@ -662,15 +661,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-categories>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-voice-assistants>
<ha-filter-blueprints
.hass=${this.hass}
.type=${"script"}
@@ -929,7 +919,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(categoryItems)
: new Set([...items].filter((x) => categoryItems!.has(x)));
} else if (
}
if (
key === "ha-filter-labels" &&
Array.isArray(filter.value) &&
filter.value.length
@@ -951,28 +942,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(labelItems)
: new Set([...items].filter((x) => labelItems!.has(x)));
} else if (
key === "ha-filter-voice-assistants" &&
Array.isArray(filter.value) &&
filter.value.length
) {
const assistItems = new Set<string>();
this.scripts
.filter((script) =>
getEntityVoiceAssistantsIds(this._entityReg, script.entity_id).some(
(va) => (filter.value as string[]).includes(va)
)
)
.forEach((script) => assistItems.add(script.entity_id));
if (!items) {
items = assistItems;
continue;
}
items =
"intersection" in items
? // @ts-ignore
items.intersection(assistItems)
: new Set([...items].filter((x) => assistItems!.has(x)));
}
}
this._filteredScripts = items ? [...items] : undefined;

View File

@@ -73,18 +73,13 @@ 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) {
if (configOptions.rows === undefined) {
mergedConfig.rows = elementOptions.rows;
}
mergedConfig.rows = elementOptions.rows;
delete mergedConfig.min_rows;
delete mergedConfig.max_rows;
}
if (elementOptions.fixed_columns) {
if (configOptions.columns === undefined) {
mergedConfig.columns = elementOptions.columns;
}
mergedConfig.columns = elementOptions.columns;
delete mergedConfig.min_columns;
delete mergedConfig.max_columns;
}

View File

@@ -11,10 +11,7 @@ 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 {
PREVIEW_CLICK_CALLBACK,
type PictureElementsCardConfig,
} from "./types";
import type { PictureElementsCardConfig } from "./types";
import type { PersonEntity } from "../../../data/person";
@customElement("hui-picture-elements-card")
@@ -169,7 +166,6 @@ 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>
@@ -225,19 +221,6 @@ 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 {

View File

@@ -487,10 +487,6 @@ 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;
@@ -505,7 +501,6 @@ 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 {

View File

@@ -15,16 +15,12 @@ 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 {
PREVIEW_CLICK_CALLBACK,
type PictureElementsCardConfig,
} from "../../cards/types";
import type { PictureElementsCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
@@ -32,6 +28,7 @@ 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({
@@ -69,44 +66,6 @@ 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) =>
[
@@ -179,16 +138,6 @@ 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}
@@ -232,7 +181,6 @@ 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 });
}
@@ -243,8 +191,7 @@ export class HuiPictureElementsCardEditor
const config = {
...this._config,
elements: ev.detail.elements as LovelaceElementConfig[],
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
} as PictureElementsCardConfig;
} as LovelaceCardConfig;
fireEvent(this, "config-changed", { config });
@@ -285,12 +232,7 @@ export class HuiPictureElementsCardEditor
elementConfig: value,
};
fireEvent(this, "config-changed", {
config: {
...this._config,
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
},
});
fireEvent(this, "config-changed", { config: this._config });
}
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {

View File

@@ -10,9 +10,7 @@ export const getElementStubConfig = async (
): Promise<LovelaceElementConfig> => {
let elementConfig: LovelaceElementConfig = { type };
if (type === "conditional") {
elementConfig = { type, conditions: [], elements: [] };
} else {
if (type !== "conditional") {
elementConfig.style = { left: "50%", top: "50%" };
}

View File

@@ -89,11 +89,7 @@ export abstract class HuiElementEditor<
}
public set value(config: T | undefined) {
// Compare symbols to detect callback changes (e.g., preview click handlers)
if (
this._config &&
deepEqual(config, this._config, { compareSymbols: true })
) {
if (this._config && deepEqual(config, this._config)) {
return;
}
this._config = config;

View File

@@ -15,11 +15,7 @@ import { tags } from "@lezer/highlight";
export { autocompletion } from "@codemirror/autocomplete";
export { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
export { highlightingFor, foldGutter } from "@codemirror/language";
export {
highlightSelectionMatches,
search,
searchKeymap,
} from "@codemirror/search";
export { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
export { EditorState } from "@codemirror/state";
export {
crosshairCursor,

View File

@@ -8326,7 +8326,6 @@
"dark_mode_image": "Dark mode image path",
"state_filter": "State filter",
"dark_mode_filter": "Dark mode state filter",
"position_hint": "Click on the image preview to position this element",
"element_types": {
"state-badge": "State badge",
"state-icon": "State icon",

View File

@@ -9175,7 +9175,7 @@ __metadata:
typescript: "npm:5.9.3"
typescript-eslint: "npm:8.52.0"
ua-parser-js: "npm:2.0.7"
vite-tsconfig-paths: "npm:6.0.4"
vite-tsconfig-paths: "npm:6.0.3"
vitest: "npm:4.0.16"
vue: "npm:2.7.16"
vue2-daterange-picker: "npm:0.6.8"
@@ -14426,26 +14426,25 @@ __metadata:
languageName: node
linkType: hard
"vite-tsconfig-paths@npm:6.0.4":
version: 6.0.4
resolution: "vite-tsconfig-paths@npm:6.0.4"
"vite-tsconfig-paths@npm:6.0.3":
version: 6.0.3
resolution: "vite-tsconfig-paths@npm:6.0.3"
dependencies:
debug: "npm:^4.1.1"
globrex: "npm:^0.1.2"
tsconfck: "npm:^3.0.3"
vite: "npm:*"
peerDependencies:
vite: "*"
peerDependenciesMeta:
vite:
optional: true
checksum: 10/85f871cd5e321f2865972559b01c518664e6e34f9039b630dd77c2f379f8fdc386e15f7237aa5c108d813030c6e9bc8edfbf61687df7684803111a2495edadac
checksum: 10/271cdc040a03181d13e4253e99ec6deec1cbe4ec4cb2a1377d46dba403483f8e38a5c2ed29ad0493ed51c2cfd658847cb1cdc73e438d6a8bcdd2b52df19d72e2
languageName: node
linkType: hard
"vite@npm:*, vite@npm:^6.0.0 || ^7.0.0":
version: 7.3.1
resolution: "vite@npm:7.3.1"
"vite@npm:^6.0.0 || ^7.0.0":
version: 7.3.0
resolution: "vite@npm:7.3.0"
dependencies:
esbuild: "npm:^0.27.0"
fdir: "npm:^6.5.0"
@@ -14494,7 +14493,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
checksum: 10/62e48ffa4283b688f0049005405a004447ad38ffc99a0efea4c3aa9b7eed739f7402b43f00668c0ee5a895b684dc953d62f0722d8a92c5b2f6c95f051bceb208
checksum: 10/044490133aaf4cc024700995edaac10ff182262c721a44a8ac7839207b3e5af435c8b9dd8ee4658dc0f47147fa4211632cca3120177aa4bab99a7cb095e5149e
languageName: node
linkType: hard