Update Scene Editor (#22847)

* New scene editor design

* bugfixes

* reorder overflow menu, change label

* category support
This commit is contained in:
karwosts 2024-11-20 06:12:23 -08:00 committed by GitHub
parent b8e2298cdd
commit 37d2c6844a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 556 additions and 239 deletions

View File

@ -338,6 +338,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
.hass=${this.hass}
narrow
.items=${[
{
path: mdiPlay,
label: this.hass.localize(
"ui.panel.config.scene.picker.apply"
),
action: () => this._activateScene(scene),
},
{
path: mdiInformationOutline,
label: this.hass.localize(
@ -352,13 +359,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
),
action: () => this._openSettings(scene),
},
{
path: mdiPlay,
label: this.hass.localize(
"ui.panel.config.scene.picker.activate"
),
action: () => this._activateScene(scene),
},
{
path: mdiTag,
label: this.hass.localize(

View File

@ -1,10 +1,15 @@
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list";
import {
mdiCheck,
mdiCog,
mdiContentDuplicate,
mdiContentSave,
mdiDelete,
mdiDotsVertical,
mdiInformationOutline,
mdiPlay,
mdiTag,
} from "@mdi/js";
import type { HassEvent } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
@ -18,10 +23,14 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import { afterNextRender } from "../../../common/util/render-status";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import "../../../components/device/ha-device-picker";
import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-area-picker";
import "../../../components/ha-button-menu";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
@ -99,6 +108,8 @@ export class HaSceneEditor extends SubscribeMixin(
@state() private _errors?: string;
@state() private _yamlErrors?: string;
@state() private _config?: SceneConfig;
@state() private _entities: string[] = [];
@ -115,6 +126,8 @@ export class HaSceneEditor extends SubscribeMixin(
@state() private _scene?: SceneEntity;
@state() private _mode: "review" | "live" | "yaml" = "review";
private _storedStates: SceneEntities = {};
private _unsubscribeEvents?: () => void;
@ -139,6 +152,16 @@ export class HaSceneEditor extends SubscribeMixin(
}
);
private _getCategory = memoizeOne(
(entries: EntityRegistryEntry[], entity_id: string | undefined) => {
if (!entity_id) {
return undefined;
}
const entry = entries.find((ent) => ent.entity_id === entity_id);
return entry?.categories?.scene;
}
);
private _getEntitiesDevices = memoizeOne(
(
entities: string[],
@ -204,13 +227,6 @@ export class HaSceneEditor extends SubscribeMixin(
if (!this.hass) {
return nothing;
}
const { devices, entities } = this._getEntitiesDevices(
this._entities,
this._devices,
this._deviceEntityLookup,
this._deviceRegistryEntries
);
return html`
<hass-subpage
.hass=${this.hass}
@ -232,6 +248,72 @@ export class HaSceneEditor extends SubscribeMixin(
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item
graphic="icon"
.disabled=${!this.sceneId || this._mode === "live"}
>
${this.hass.localize("ui.panel.config.scene.picker.apply")}
<ha-svg-icon
class="selected_menu_item"
slot="graphic"
.path=${mdiPlay}
></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
${this.hass.localize("ui.panel.config.scene.picker.show_info")}
<ha-svg-icon
class="selected_menu_item"
slot="graphic"
.path=${mdiInformationOutline}
></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
${this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
)}
<ha-svg-icon
class="selected_menu_item"
slot="graphic"
.path=${mdiCog}
></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
${this.hass.localize(
`ui.panel.config.scene.picker.${this._getCategory(this._entityRegistryEntries, this._scene?.entity_id) ? "edit_category" : "assign_category"}`
)}
<ha-svg-icon
class="selected_menu_item"
slot="graphic"
.path=${mdiTag}
></ha-svg-icon>
</ha-list-item>
<li divider role="separator"></li>
<ha-list-item graphic="icon">
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")}
${this._mode !== "yaml"
? html`<ha-svg-icon
class="selected_menu_item"
slot="graphic"
.path=${mdiCheck}
></ha-svg-icon>`
: nothing}
</ha-list-item>
<ha-list-item graphic="icon">
${this.hass.localize("ui.panel.config.automation.editor.edit_yaml")}
${this._mode === "yaml"
? html`<ha-svg-icon
class="selected_menu_item"
slot="graphic"
.path=${mdiCheck}
></ha-svg-icon>`
: nothing}
</ha-list-item>
<li divider role="separator"></li>
<ha-list-item .disabled=${!this.sceneId} graphic="icon">
${this.hass.localize(
"ui.panel.config.scene.picker.duplicate_scene"
@ -257,208 +339,7 @@ export class HaSceneEditor extends SubscribeMixin(
</ha-list-item>
</ha-button-menu>
${this._errors ? html` <div class="errors">${this._errors}</div> ` : ""}
<div
id="root"
class=${classMap({
rtl: computeRTL(this.hass),
})}
>
${this._config
? html`
<div
class=${classMap({
container: true,
narrow: !this.isWide,
})}
>
<ha-card outlined>
<div class="card-content">
<ha-textfield
.value=${this._config.name}
.name=${"name"}
@change=${this._valueChanged}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.name"
)}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.icon"
)}
.name=${"icon"}
.value=${this._config.icon}
@value-changed=${this._valueChanged}
>
</ha-icon-picker>
<ha-area-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.area"
)}
.name=${"area"}
.value=${this._sceneAreaIdWithUpdates || ""}
@value-changed=${this._areaChanged}
>
</ha-area-picker>
</div>
</ha-card>
</div>
<ha-config-section vertical .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize(
"ui.panel.config.scene.editor.devices.header"
)}
</div>
<div slot="introduction">
${this.hass.localize(
"ui.panel.config.scene.editor.devices.introduction"
)}
</div>
${devices.map(
(device) => html`
<ha-card outlined>
<h1 class="card-header">
${device.name}
<ha-icon-button
.path=${mdiDelete}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.devices.delete"
)}
.device=${device.id}
@click=${this._deleteDevice}
></ha-icon-button>
</h1>
<mwc-list>
${device.entities.map((entityId) => {
const entityStateObj = this.hass.states[entityId];
if (!entityStateObj) {
return nothing;
}
return html`
<ha-list-item
hasMeta
graphic="icon"
.entityId=${entityId}
@click=${this._showMoreInfo}
>
<state-badge
.hass=${this.hass}
.stateObj=${entityStateObj}
slot="graphic"
></state-badge>
${computeStateName(entityStateObj)}
</ha-list-item>
`;
})}
</mwc-list>
</ha-card>
`
)}
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.scene.editor.devices.add"
)}
>
<div class="card-content">
<ha-device-picker
@value-changed=${this._devicePicked}
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.devices.add"
)}
></ha-device-picker>
</div>
</ha-card>
</ha-config-section>
${this.showAdvanced
? html`
<ha-config-section vertical .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize(
"ui.panel.config.scene.editor.entities.header"
)}
</div>
<div slot="introduction">
${this.hass.localize(
"ui.panel.config.scene.editor.entities.introduction"
)}
</div>
${entities.length
? html`
<ha-card
outlined
class="entities"
.header=${this.hass.localize(
"ui.panel.config.scene.editor.entities.without_device"
)}
>
<mwc-list>
${entities.map((entityId) => {
const entityStateObj =
this.hass.states[entityId];
if (!entityStateObj) {
return nothing;
}
return html`
<ha-list-item
hasMeta
graphic="icon"
.entityId=${entityId}
@click=${this._showMoreInfo}
>
<state-badge
.hass=${this.hass}
.stateObj=${entityStateObj}
slot="graphic"
></state-badge>
${computeStateName(entityStateObj)}
<div slot="meta">
<ha-icon-button
.path=${mdiDelete}
.entityId=${entityId}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.entities.delete"
)}
@click=${this._deleteEntity}
></ha-icon-button>
</div>
</ha-list-item>
`;
})}
</mwc-list>
</ha-card>
`
: ""}
<ha-card
outlined
header=${this.hass.localize(
"ui.panel.config.scene.editor.entities.add"
)}
>
<div class="card-content">
<ha-entity-picker
@value-changed=${this._entityPicked}
.excludeDomains=${SCENE_IGNORED_DOMAINS}
.hass=${this.hass}
label=${this.hass.localize(
"ui.panel.config.scene.editor.entities.add"
)}
></ha-entity-picker>
</div>
</ha-card>
</ha-config-section>
`
: ""}
`
: ""}
</div>
${this._mode === "yaml" ? this.renderYamlMode() : this.renderUiMode()}
<ha-fab
slot="fab"
.label=${this.hass.localize("ui.panel.config.scene.editor.save")}
@ -473,6 +354,270 @@ export class HaSceneEditor extends SubscribeMixin(
`;
}
private renderYamlMode() {
return html` <ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._config}
@value-changed=${this._yamlChanged}
></ha-yaml-editor>`;
}
private renderUiMode() {
const { devices, entities } = this._getEntitiesDevices(
this._entities,
this._devices,
this._deviceEntityLookup,
this._deviceRegistryEntries
);
return html` <div
id="root"
class=${classMap({
rtl: computeRTL(this.hass),
})}
>
${this._config
? html`
<div
class=${classMap({
container: true,
narrow: !this.isWide,
})}
>
${this._mode === "live"
? html` <ha-alert
alert-type="info"
.title=${this.hass.localize(
"ui.panel.config.scene.editor.live_preview"
)}
>
${this.hass.localize(
"ui.panel.config.scene.editor.live_preview_detail"
)}
<ha-button slot="action" @click=${this._exitLiveMode}
>${this.hass.localize(
"ui.panel.config.scene.editor.back_to_review_mode"
)}</ha-button
>
</ha-alert>`
: html` <ha-alert
alert-type="info"
.title=${this.hass.localize(
"ui.panel.config.scene.editor.review_mode"
)}
>
${this.hass.localize(
"ui.panel.config.scene.editor.review_mode_detail"
)}
<ha-button slot="action" @click=${this._enterLiveMode}
>${this.hass.localize(
"ui.panel.config.scene.editor.live_preview"
)}</ha-button
>
</ha-alert>`}
<ha-card outlined>
<div class="card-content">
<ha-textfield
.value=${this._config.name}
.name=${"name"}
@change=${this._valueChanged}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.name"
)}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.icon"
)}
.name=${"icon"}
.value=${this._config.icon}
@value-changed=${this._valueChanged}
>
</ha-icon-picker>
<ha-area-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.area"
)}
.name=${"area"}
.value=${this._sceneAreaIdWithUpdates || ""}
@value-changed=${this._areaChanged}
>
</ha-area-picker>
</div>
</ha-card>
</div>
<ha-config-section vertical .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize(
"ui.panel.config.scene.editor.devices.header"
)}
</div>
${this._mode === "live"
? html` <div slot="introduction">
${this.hass.localize(
"ui.panel.config.scene.editor.devices.introduction"
)}
</div>`
: nothing}
${devices.map(
(device) => html`
<ha-card outlined>
<h1 class="card-header">
${device.name}
<ha-icon-button
.path=${mdiDelete}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.devices.delete"
)}
.device=${device.id}
@click=${this._deleteDevice}
></ha-icon-button>
</h1>
<mwc-list>
${device.entities.map((entityId) => {
const entityStateObj = this.hass.states[entityId];
if (!entityStateObj) {
return nothing;
}
return html`
<ha-list-item
hasMeta
.graphic=${this._mode === "live"
? "icon"
: undefined}
.entityId=${entityId}
@click=${this._showMoreInfo}
>
${this._mode === "live"
? html`
<state-badge
.hass=${this.hass}
.stateObj=${entityStateObj}
slot="graphic"
></state-badge>
`
: nothing}
${computeStateName(entityStateObj)}
</ha-list-item>
`;
})}
</mwc-list>
</ha-card>
`
)}
${this._mode === "live"
? html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.scene.editor.devices.add"
)}
>
<div class="card-content">
<ha-device-picker
@value-changed=${this._devicePicked}
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.devices.add"
)}
></ha-device-picker>
</div>
</ha-card>
`
: nothing}
</ha-config-section>
${this.showAdvanced
? html` <ha-config-section vertical .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize(
"ui.panel.config.scene.editor.entities.header"
)}
</div>
${this._mode === "live"
? html` <div slot="introduction">
${this.hass.localize(
"ui.panel.config.scene.editor.entities.introduction"
)}
</div>`
: nothing}
${entities.length
? html`
<ha-card
outlined
class="entities"
.header=${this.hass.localize(
"ui.panel.config.scene.editor.entities.without_device"
)}
>
<mwc-list>
${entities.map((entityId) => {
const entityStateObj = this.hass.states[entityId];
if (!entityStateObj) {
return nothing;
}
return html`
<ha-list-item
hasMeta
.graphic=${this._mode === "live"
? "icon"
: undefined}
.entityId=${entityId}
@click=${this._showMoreInfo}
>
${this._mode === "live"
? html` <state-badge
.hass=${this.hass}
.stateObj=${entityStateObj}
slot="graphic"
></state-badge>`
: nothing}
${computeStateName(entityStateObj)}
<div slot="meta">
<ha-icon-button
.path=${mdiDelete}
.entityId=${entityId}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.entities.delete"
)}
@click=${this._deleteEntity}
></ha-icon-button>
</div>
</ha-list-item>
`;
})}
</mwc-list>
</ha-card>
`
: ""}
${this._mode === "live"
? html` <ha-card
outlined
header=${this.hass.localize(
"ui.panel.config.scene.editor.entities.add"
)}
>
<div class="card-content">
<ha-entity-picker
@value-changed=${this._entityPicked}
.excludeDomains=${SCENE_IGNORED_DOMAINS}
.hass=${this.hass}
label=${this.hass.localize(
"ui.panel.config.scene.editor.entities.add"
)}
></ha-entity-picker>
</div>
</ha-card>`
: nothing}
</ha-config-section>`
: nothing}
`
: nothing}
</div>`;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
@ -522,47 +667,151 @@ export class HaSceneEditor extends SubscribeMixin(
this._deviceEntityLookup[entity.device_id].push(entity.entity_id);
if (
this._entities.includes(entity.entity_id) &&
!this._single_entities.includes(entity.device_id) &&
!this._single_entities.includes(entity.entity_id) &&
!this._devices.includes(entity.device_id)
) {
this._devices = [...this._devices, entity.device_id];
}
}
}
if (
changedProps.has("scenes") &&
this.sceneId &&
this._config &&
!this._scene
) {
this._setScene();
}
if (this._scenesSet && changedProps.has("scenes")) {
this._scenesSet();
}
if (changedProps.has("hass")) {
if (this._scene) {
if (this.hass.states[this._scene.entity_id] !== this._scene) {
this._scene = this.hass.states[this._scene.entity_id];
}
} else if (this.sceneId) {
this._scene = Object.values(this.hass.states).find(
(stateObj) =>
stateObj.entity_id.startsWith("scene") &&
stateObj.attributes?.id === this.sceneId
);
}
}
}
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._duplicate();
activateScene(this.hass, this._scene!.entity_id);
break;
case 1:
fireEvent(this, "hass-more-info", { entityId: this._scene!.entity_id });
break;
case 2:
showMoreInfoDialog(this, {
entityId: this._scene!.entity_id,
view: "settings",
});
break;
case 3:
this._editCategory(this._scene!);
break;
case 4:
if (this._mode === "yaml") {
this._initEntities(this._config!);
this._exitYamlMode();
}
break;
case 5:
if (this._mode !== "yaml") {
this._enterYamlMode();
}
break;
case 6:
this._duplicate();
break;
case 7:
this._deleteTapped();
break;
}
}
private async _setScene() {
const scene = this.scenes.find(
(entity: SceneEntity) => entity.attributes.id === this.sceneId
);
if (!scene) {
private async _exitYamlMode() {
if (this._yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this._yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this._yamlErrors = undefined;
this._mode = "review";
}
private _enterYamlMode() {
if (this._mode === "live") {
this._generateConfigFromLive();
if (this._unsubscribeEvents) {
this._unsubscribeEvents();
this._unsubscribeEvents = undefined;
}
applyScene(this.hass, this._storedStates);
}
this._mode = "yaml";
}
private async _enterLiveMode() {
if (this._dirty) {
const result = await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.scene.editor.enter_live_mode_unsaved"
),
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this._entities.forEach((entity) => this._storeState(entity));
this._mode = "live";
await this._setScene();
this._subscribeEvents();
}
private _exitLiveMode() {
this._generateConfigFromLive();
if (this._unsubscribeEvents) {
this._unsubscribeEvents();
this._unsubscribeEvents = undefined;
}
applyScene(this.hass, this._storedStates);
this._mode = "review";
}
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
this._dirty = true;
if (!ev.detail.isValid) {
this._yamlErrors = ev.detail.errorMsg;
return;
}
this._yamlErrors = undefined;
this._config = ev.detail.value;
this._errors = undefined;
}
private async _setScene() {
if (!this._scene) {
return;
}
this._scene = scene;
const { context } = await activateScene(this.hass, this._scene.entity_id);
this._activateContextId = context.id;
}
private async _subscribeEvents() {
this._unsubscribeEvents =
await this.hass!.connection.subscribeEvents<HassEvent>(
(event) => this._stateChanged(event),
@ -601,7 +850,9 @@ export class HaSceneEditor extends SubscribeMixin(
this._initEntities(config);
this._setScene();
this._scene = this.scenes.find(
(entity: SceneEntity) => entity.attributes.id === this.sceneId
);
this._dirty = false;
this._config = config;
@ -609,7 +860,6 @@ export class HaSceneEditor extends SubscribeMixin(
private _initEntities(config: SceneConfig) {
this._entities = Object.keys(config.entities);
this._entities.forEach((entity) => this._storeState(entity));
this._single_entities = [];
const filteredEntityReg = this._entityRegistryEntries.filter((entityReg) =>
@ -665,6 +915,12 @@ export class HaSceneEditor extends SubscribeMixin(
this._single_entities = this._single_entities.filter(
(entityId) => entityId !== deleteEntityId
);
if (this._config!.entities) {
delete this._config!.entities[deleteEntityId];
}
if (this._config!.metadata) {
delete this._config!.metadata[deleteEntityId];
}
this._dirty = true;
}
@ -700,6 +956,11 @@ export class HaSceneEditor extends SubscribeMixin(
this._entities = this._entities.filter(
(entityId) => !deviceEntities.includes(entityId)
);
if (this._config!.entities) {
deviceEntities.forEach((entityId) => {
delete this._config!.entities[entityId];
});
}
this._dirty = true;
}
@ -758,7 +1019,9 @@ export class HaSceneEditor extends SubscribeMixin(
};
private _goBack(): void {
applyScene(this.hass, this._storedStates);
if (this._mode === "live") {
applyScene(this.hass, this._storedStates);
}
afterNextRender(() => history.back());
}
@ -780,7 +1043,9 @@ export class HaSceneEditor extends SubscribeMixin(
private async _delete(): Promise<void> {
await deleteScene(this.hass, this.sceneId!);
applyScene(this.hass, this._storedStates);
if (this._mode === "live") {
applyScene(this.hass, this._storedStates);
}
history.back();
}
@ -865,16 +1130,29 @@ export class HaSceneEditor extends SubscribeMixin(
return { ...stateObj.attributes, state: stateObj.state };
}
private async _saveScene(): Promise<void> {
const id = !this.sceneId ? "" + Date.now() : this.sceneId!;
private _generateConfigFromLive() {
this._config = {
...this._config!,
entities: this._calculateStates(),
metadata: this._calculateMetaData(),
};
}
private async _saveScene(): Promise<void> {
if (this._yamlErrors) {
showToast(this, {
message: this._yamlErrors,
});
return;
}
const id = !this.sceneId ? "" + Date.now() : this.sceneId!;
if (this._mode === "live") {
this._generateConfigFromLive();
}
try {
this._saving = true;
await saveScene(this.hass, id, this._config);
await saveScene(this.hass, id, this._config!);
if (this._updatedAreaId !== undefined) {
let scene =
@ -943,6 +1221,27 @@ export class HaSceneEditor extends SubscribeMixin(
: undefined;
}
private _editCategory(scene: any) {
const entityReg = this._entityRegistryEntries.find(
(reg) => reg.entity_id === scene.entity_id
);
if (!entityReg) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.scene.picker.no_category_support"
),
text: this.hass.localize(
"ui.panel.config.scene.picker.no_category_entity_reg"
),
});
return;
}
showAssignCategoryDialog(this, {
scope: "scene",
entityReg,
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
@ -982,6 +1281,14 @@ export class HaSceneEditor extends SubscribeMixin(
bottom: calc(-80px - env(safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-alert {
display: block;
margin-bottom: 24px;
}
ha-button {
white-space: nowrap;
--mdc-theme-primary: var(--primary-color);
}
ha-fab.dirty {
bottom: 0;
}
@ -1002,6 +1309,9 @@ export class HaSceneEditor extends SubscribeMixin(
justify-content: center;
align-items: center;
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
`,
];
}

View File

@ -3857,6 +3857,7 @@
"edit_scene": "Edit scene",
"show_info": "[%key:ui::panel::config::automation::editor::show_info%]",
"activate": "Activate",
"apply": "Apply",
"delete_scene": "Delete scene",
"delete": "[%key:ui::common::delete%]",
"delete_confirm_title": "Delete scene?",
@ -3881,6 +3882,12 @@
"search": "Search {number} scenes"
},
"editor": {
"review_mode": "Review Mode",
"review_mode_detail": "You can adjust the scene's details and remove devices or entities. To fully edit, switch to Live Preview, which will apply the scene.",
"live_preview": "Live Preview",
"live_preview_detail": "In Live Preview, all changes to this scene are applied in real-time to your devices and entities.",
"enter_live_mode_unsaved": "You have unsaved changes to this scene. Continuing to live preview will apply the saved scene, which may overwrite your unsaved changes. Consider if you would like to save the scene first before activating it.",
"back_to_review_mode": "Back to review mode",
"default_name": "New scene",
"load_error_not_editable": "Only scenes in scenes.yaml are editable.",
"load_error_unknown": "Error loading scene ({err_no}).",