Compare commits

..

15 Commits

Author SHA1 Message Date
Wendelin
d808c63c55 Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-new-design 2025-10-16 16:29:10 +02:00
Aidan Timson
f32ca9be29 Set header bar min height and make sure items are centered (#27542) 2025-10-16 16:14:08 +02:00
Aidan Timson
8c4c4157a8 Remove unnecessary on-surface-default semantic color (#27536) 2025-10-16 16:07:26 +02:00
Wendelin
c8419d4c3d Improve target picker section title (#27539) 2025-10-16 15:37:04 +02:00
Wendelin
1da3570a6f fix translations, scroll issues, RTL 2025-10-16 15:01:57 +02:00
Wendelin
e97b04b5b9 Fix device translation 2025-10-16 10:19:28 +02:00
Wendelin
6dbfb60825 Add keybindings and blocks search separation 2025-10-16 09:44:46 +02:00
Wendelin
ba2881b0bf Add max-height 2025-10-15 17:00:06 +02:00
Wendelin
e42be13682 fix height 2025-10-15 16:53:32 +02:00
Wendelin
bf8dacb7a4 Add tabs 2025-10-15 16:50:18 +02:00
Wendelin
a23fc8fcba revert merge 2025-10-14 17:07:48 +02:00
Wendelin
0ba88246a1 Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-new-design 2025-10-14 17:01:30 +02:00
Wendelin
6529b31b65 WIP new add dialog 2025-10-14 16:59:46 +02:00
Wendelin
866518477f Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-new-design 2025-10-13 13:13:41 +02:00
Wendelin
f831f876de WIP new add automation element 2025-10-10 14:48:18 +02:00
26 changed files with 1004 additions and 576 deletions

View File

@@ -20,7 +20,6 @@ import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-input-helper-text";
import "../ha-sortable";
interface EntityNameOption {
@@ -240,6 +239,7 @@ export class HaEntityNamePicker extends LitElement {
.autofocus=${this.autofocus}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.helper=${this.helper}
.items=${options}
allow-custom-value
item-id-path="value"
@@ -253,20 +253,9 @@ export class HaEntityNamePicker extends LitElement {
</ha-combo-box>
</mwc-menu-surface>
</div>
${this._renderHelper()}
`;
}
private _renderHelper() {
return this.helper
? html`
<ha-input-helper-text .disabled=${this.disabled}>
${this.helper}
</ha-input-helper-text>
`
: nothing;
}
private _onClosed(ev) {
ev.stopPropagation();
this._opened = false;
@@ -521,11 +510,6 @@ export class HaEntityNamePicker extends LitElement {
.sortable-drag {
cursor: grabbing;
}
ha-input-helper-text {
display: block;
margin: var(--ha-space-2) 0 0;
}
`;
}

View File

@@ -1,6 +1,7 @@
import "@home-assistant/webawesome/dist/components/drawer/drawer";
import { css, html, LitElement, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { haStyleScrollbar } from "../resources/styles";
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
@@ -37,49 +38,61 @@ export class HaBottomSheet extends LitElement {
@wa-after-hide=${this._handleAfterHide}
without-header
>
<slot></slot>
<slot name="header"></slot>
<div class="body ha-scrollbar">
<slot></slot>
</div>
</wa-drawer>
`;
}
static styles = css`
wa-drawer {
--wa-color-surface-raised: transparent;
--spacing: 0;
--size: var(--ha-bottom-sheet-height, auto);
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
}
wa-drawer::part(dialog) {
max-height: var(--ha-bottom-sheet-max-height, 90vh);
align-items: center;
}
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
width: 100%;
border-top-left-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
border-top-right-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
background-color: var(
--ha-bottom-sheet-surface-background,
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
);
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
:host([flexcontent]) wa-drawer::part(body) {
display: flex;
}
`;
static styles = [
haStyleScrollbar,
css`
wa-drawer {
--wa-color-surface-raised: transparent;
--spacing: 0;
--size: var(--ha-bottom-sheet-height, auto);
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
}
wa-drawer::part(dialog) {
max-height: var(--ha-bottom-sheet-max-height, 90vh);
align-items: center;
}
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
width: 100%;
border-top-left-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
border-top-right-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
background-color: var(
--ha-bottom-sheet-surface-background,
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
);
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
:host([flexcontent]) wa-drawer::part(body) {
display: flex;
flex-direction: column;
}
:host([flexcontent]) .body {
flex: 1;
max-width: 100%;
display: flex;
flex-direction: column;
}
`,
];
}
declare global {

View File

@@ -31,6 +31,9 @@ export class HaButtonToggleGroup extends LitElement {
@property({ type: Boolean, reflect: true, attribute: "no-wrap" })
public nowrap = false;
@property({ type: Boolean, reflect: true, attribute: "full-width" })
public fullWidth = false;
@property() public variant:
| "brand"
| "neutral"
@@ -38,6 +41,13 @@ export class HaButtonToggleGroup extends LitElement {
| "warning"
| "danger" = "brand";
@property({ attribute: "active-variant" }) public activeVariant?:
| "brand"
| "neutral"
| "success"
| "warning"
| "danger";
protected render(): TemplateResult {
return html`
<wa-button-group childSelector="ha-button">
@@ -46,7 +56,9 @@ export class HaButtonToggleGroup extends LitElement {
html`<ha-button
iconTag="ha-svg-icon"
class="icon"
.variant=${this.variant}
.variant=${this.active !== button.value || !this.activeVariant
? this.variant
: this.activeVariant}
.size=${this.size}
.value=${button.value}
@click=${this._handleClick}
@@ -78,6 +90,19 @@ export class HaButtonToggleGroup extends LitElement {
:host([no-wrap]) wa-button-group::part(base) {
flex-wrap: nowrap;
}
wa-button-group {
padding: var(--ha-button-toggle-group-padding);
}
:host([full-width]) wa-button-group,
:host([full-width]) wa-button-group::part(base) {
width: 100%;
}
:host([full-width]) ha-button {
flex: 1;
}
`;
}

View File

@@ -49,12 +49,16 @@ export class HaDialogHeader extends LitElement {
display: flex;
flex-direction: row;
align-items: center;
padding: 4px;
padding: 0 var(--ha-space-1);
box-sizing: border-box;
}
.header-content {
flex: 1;
padding: 10px 4px;
padding: 10px var(--ha-space-1);
display: flex;
flex-direction: column;
justify-content: center;
min-height: var(--ha-space-12);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
@@ -63,7 +67,7 @@ export class HaDialogHeader extends LitElement {
.header-title {
height: var(
--ha-dialog-header-title-height,
calc(var(--ha-font-size-xl) + 4px)
calc(var(--ha-font-size-xl) + var(--ha-space-1))
);
font-size: var(--ha-font-size-xl);
line-height: var(--ha-line-height-condensed);
@@ -76,19 +80,19 @@ export class HaDialogHeader extends LitElement {
}
@media all and (min-width: 450px) and (min-height: 500px) {
.header-bar {
padding: 16px;
padding: 0 var(--ha-space-2);
}
}
.header-navigation-icon {
flex: none;
min-width: 8px;
min-width: var(--ha-space-2);
height: 100%;
display: flex;
flex-direction: row;
}
.header-action-items {
flex: none;
min-width: 8px;
min-width: var(--ha-space-2);
height: 100%;
display: flex;
flex-direction: row;

View File

@@ -179,7 +179,7 @@ export class HaGenericPicker extends LitElement {
}
ha-input-helper-text {
display: block;
margin: var(--ha-space-2) 0 0;
margin: 8px 0 0;
}
`,
];

View File

@@ -1,12 +1,12 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { mdiClose } from "@mdi/js";
import "./ha-dialog-header";
import "./ha-icon-button";
import type { HomeAssistant } from "../types";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-dialog-header";
import "./ha-icon-button";
export type DialogWidth = "small" | "medium" | "large" | "full";
@@ -90,6 +90,8 @@ export class HaWaDialog extends LitElement {
@state()
private _open = false;
@query(".body") public bodyContainer!: HTMLDivElement;
protected updated(
changedProperties: Map<string | number | symbol, unknown>
): void {
@@ -107,6 +109,7 @@ export class HaWaDialog extends LitElement {
.lightDismiss=${!this.preventScrimClose}
without-header
@wa-show=${this._handleShow}
@wa-after-show=${this._handleAfterShow}
@wa-after-hide=${this._handleAfterHide}
>
<slot name="header">
@@ -146,6 +149,10 @@ export class HaWaDialog extends LitElement {
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
};
private _handleAfterShow = () => {
fireEvent(this, "after-show");
};
private _handleAfterHide = () => {
this._open = false;
fireEvent(this, "closed");
@@ -172,7 +179,7 @@ export class HaWaDialog extends LitElement {
)
)
);
--width: var(--ha-dialog-width-md, min(580px, var(--full-width)));
--width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
--spacing: var(--dialog-content-padding, var(--ha-space-6));
--show-duration: var(--ha-dialog-show-duration, 200ms);
--hide-duration: var(--ha-dialog-hide-duration, 200ms);
@@ -193,11 +200,11 @@ export class HaWaDialog extends LitElement {
}
:host([width="small"]) wa-dialog {
--width: var(--ha-dialog-width-sm, min(320px, var(--full-width)));
--width: min(var(--ha-dialog-width-sm, 320px), var(--full-width));
}
:host([width="large"]) wa-dialog {
--width: var(--ha-dialog-width-lg, min(720px, var(--full-width)));
--width: min(var(--ha-dialog-width-lg, 720px), var(--full-width));
}
:host([width="full"]) wa-dialog {
@@ -211,6 +218,7 @@ export class HaWaDialog extends LitElement {
--ha-dialog-max-height,
calc(100% - var(--ha-space-20))
);
min-height: var(--ha-dialog-min-height);
position: var(--dialog-surface-position, relative);
margin-top: var(--dialog-surface-margin-top, auto);
display: flex;
@@ -247,10 +255,7 @@ export class HaWaDialog extends LitElement {
.header-title {
margin: 0;
margin-bottom: 0;
color: var(
--ha-dialog-header-title-color,
var(--ha-color-on-surface-default, var(--primary-text-color))
);
color: var(--ha-dialog-header-title-color, var(--primary-text-color));
font-size: var(
--ha-dialog-header-title-font-size,
var(--ha-font-size-2xl)
@@ -287,6 +292,7 @@ export class HaWaDialog extends LitElement {
}
:host([flexcontent]) .body {
max-width: 100%;
flex: 1;
display: flex;
flex-direction: column;
}
@@ -315,6 +321,7 @@ declare global {
interface HASSDomEvents {
opened: undefined;
"after-show": undefined;
closed: undefined;
}
}

View File

@@ -636,9 +636,6 @@ export class HaTargetPickerItemRow extends LitElement {
font-size: var(--ha-font-size-s);
color: var(--secondary-text-color);
}
.domain {
font-family: var(--ha-font-family-code);
}
.entries-tree {
display: flex;

View File

@@ -1019,10 +1019,14 @@ export class HaTargetPickerSelector extends LitElement {
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
}
.title {
width: 100%;
min-height: var(--ha-space-8);
}
:host([mode="dialog"]) .title {

View File

@@ -6,8 +6,6 @@ import {
mdiCallSplit,
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiFormatListNumbered,
mdiGestureDoubleTap,
mdiHandBackRight,
@@ -16,10 +14,10 @@ import {
mdiRoomService,
mdiShuffleDisabled,
mdiTimerOutline,
mdiTools,
mdiTrafficLight,
} from "@mdi/js";
import type { AutomationElementGroup } from "./automation";
import type { AutomationElementGroupCollection } from "./automation";
import type { Action } from "./script";
export const ACTION_ICONS = {
condition: mdiAbTesting,
@@ -48,37 +46,73 @@ export const YAML_ONLY_ACTION_TYPES = new Set<keyof typeof ACTION_ICONS>([
"variables",
]);
export const ACTION_GROUPS: AutomationElementGroup = {
device_id: {},
helpers: {
icon: mdiTools,
members: {},
},
building_blocks: {
icon: mdiExcavator,
members: {
condition: {},
delay: {},
wait_template: {},
wait_for_trigger: {},
repeat_count: {},
repeat_while: {},
repeat_until: {},
repeat_for_each: {},
choose: {},
if: {},
stop: {},
sequence: {},
parallel: {},
variables: {},
export const ACTION_COLLECTIONS: AutomationElementGroupCollection[] = [
{
groups: {
device_id: {},
serviceGroups: {},
},
},
other: {
icon: mdiDotsHorizontal,
members: {
{
titleKey: "ui.panel.config.automation.editor.actions.groups.helpers.label",
groups: {
helpers: {},
},
},
{
titleKey: "ui.panel.config.automation.editor.actions.groups.other.label",
groups: {
event: {},
service: {},
set_conversation_response: {},
other: {},
},
},
] as const;
export const ACTION_BUILDING_BLOCKS_GROUP = {
condition: {},
delay: {},
wait_template: {},
wait_for_trigger: {},
repeat_count: {},
repeat_while: {},
repeat_until: {},
repeat_for_each: {},
choose: {},
if: {},
stop: {},
sequence: {},
parallel: {},
variables: {},
};
// These will be replaced with the correct action
export const VIRTUAL_ACTIONS: Partial<
Record<keyof typeof ACTION_BUILDING_BLOCKS_GROUP, Action>
> = {
repeat_count: {
repeat: {
count: 2,
sequence: [],
},
},
repeat_while: {
repeat: {
while: [],
sequence: [],
},
},
repeat_until: {
repeat: {
until: [],
sequence: [],
},
},
repeat_for_each: {
repeat: {
for_each: {},
sequence: [],
},
},
} as const;

View File

@@ -4,6 +4,7 @@ import type {
} from "home-assistant-js-websocket";
import { ensureArray } from "../common/array/ensure-array";
import { navigate } from "../common/navigate";
import type { LocalizeKeys } from "../common/translations/localize";
import { createSearchParam } from "../common/url/search-params";
import type { Context, HomeAssistant } from "../types";
import type { BlueprintInput } from "./blueprint";
@@ -293,6 +294,11 @@ export interface ShorthandNotCondition extends ShorthandBaseCondition {
not: Condition[];
}
export interface AutomationElementGroupCollection {
titleKey?: LocalizeKeys;
groups: AutomationElementGroup;
}
export type AutomationElementGroup = Record<
string,
{ icon?: string; members?: AutomationElementGroup }

View File

@@ -3,8 +3,6 @@ import {
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiGateOr,
mdiIdentifier,
mdiMapClock,
@@ -15,7 +13,7 @@ import {
mdiStateMachine,
mdiWeatherSunny,
} from "@mdi/js";
import type { AutomationElementGroup } from "./automation";
import type { AutomationElementGroupCollection } from "./automation";
export const CONDITION_ICONS = {
device: mdiDevices,
@@ -31,25 +29,31 @@ export const CONDITION_ICONS = {
zone: mdiMapMarkerRadius,
};
export const CONDITION_GROUPS: AutomationElementGroup = {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: { sun: {}, time: {}, zone: {} },
export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
{
groups: {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: { sun: {}, time: {}, zone: {} },
},
},
},
building_blocks: {
icon: mdiExcavator,
members: { and: {}, or: {}, not: {} },
},
other: {
icon: mdiDotsHorizontal,
members: {
{
titleKey: "ui.panel.config.automation.editor.conditions.groups.other.label",
groups: {
template: {},
trigger: {},
},
},
} as const;
] as const;
export const CONDITION_BUILDING_BLOCKS_GROUP = {
and: {},
or: {},
not: {},
};
export const CONDITION_BUILDING_BLOCKS = ["and", "or", "not"];

View File

@@ -4,7 +4,6 @@ import {
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiFormatListBulleted,
mdiGestureDoubleTap,
mdiMapClock,
@@ -23,7 +22,7 @@ import {
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import type {
AutomationElementGroup,
AutomationElementGroupCollection,
Trigger,
TriggerList,
} from "./automation";
@@ -49,16 +48,26 @@ export const TRIGGER_ICONS = {
list: mdiFormatListBulleted,
};
export const TRIGGER_GROUPS: AutomationElementGroup = {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: { calendar: {}, sun: {}, time: {}, time_pattern: {}, zone: {} },
export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
{
groups: {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: {
calendar: {},
sun: {},
time: {},
time_pattern: {},
zone: {},
},
},
},
},
other: {
icon: mdiDotsHorizontal,
members: {
{
titleKey: "ui.panel.config.automation.editor.triggers.groups.other.label",
groups: {
event: {},
geo_location: {},
homeassistant: {},
@@ -70,7 +79,7 @@ export const TRIGGER_GROUPS: AutomationElementGroup = {
persistent_notification: {},
},
},
} as const;
] as const;
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
"triggers" in trigger;

View File

@@ -1,9 +1,10 @@
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { ensureArray } from "../../../../common/array/ensure-array";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
@@ -15,19 +16,18 @@ import {
ACTION_BUILDING_BLOCKS,
getService,
isService,
VIRTUAL_ACTIONS,
} from "../../../../data/action";
import type { AutomationClipboard } from "../../../../data/automation";
import type { Action } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types";
import {
PASTE_VALUE,
VIRTUAL_ACTIONS,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
import { automationRowsStyles } from "../styles";
import type HaAutomationActionRow from "./ha-automation-action-row";
import { getAutomationActionType } from "./ha-automation-action-row";
import { ensureArray } from "../../../../common/array/ensure-array";
@customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement {
@@ -136,17 +136,6 @@ export default class HaAutomationAction extends LitElement {
"ui.panel.config.automation.editor.actions.add"
)}
</ha-button>
<ha-button
.disabled=${this.disabled}
@click=${this._addActionBuildingBlockDialog}
appearance="plain"
.size=${this.root ? "medium" : "small"}
>
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.add_building_block"
)}
</ha-button>
</div>
</div>
</ha-sortable>
@@ -222,15 +211,6 @@ export default class HaAutomationAction extends LitElement {
});
}
private _addActionBuildingBlockDialog() {
showAddAutomationElementDialog(this, {
type: "action",
add: this._addAction,
clipboardItem: getAutomationActionType(this._clipboard?.action),
group: "building_blocks",
});
}
private _addAction = (action: string) => {
let actions: Action[];
if (action === PASTE_VALUE) {

File diff suppressed because it is too large Load Diff

View File

@@ -214,17 +214,6 @@ export default class HaAutomationCondition extends LitElement {
"ui.panel.config.automation.editor.conditions.add"
)}
</ha-button>
<ha-button
.disabled=${this.disabled}
appearance="plain"
.size=${this.root ? "medium" : "small"}
@click=${this._addConditionBuildingBlockDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.add_building_block"
)}
</ha-button>
</div>
</div>
</ha-sortable>
@@ -242,15 +231,6 @@ export default class HaAutomationCondition extends LitElement {
});
}
private _addConditionBuildingBlockDialog() {
showAddAutomationElementDialog(this, {
type: "condition",
add: this._addCondition,
clipboardItem: this._clipboard?.condition?.condition,
group: "building_blocks",
});
}
private _addCondition = (value) => {
let conditions: Condition[];
if (value === PASTE_VALUE) {

View File

@@ -1,45 +1,11 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { ACTION_GROUPS } from "../../../data/action";
import type { ActionType } from "../../../data/script";
export const PASTE_VALUE = "__paste__";
// These will be replaced with the correct action
export const VIRTUAL_ACTIONS: Record<
keyof (typeof ACTION_GROUPS)["building_blocks"]["members"],
ActionType
> = {
repeat_count: {
repeat: {
count: 2,
sequence: [],
},
},
repeat_while: {
repeat: {
while: [],
sequence: [],
},
},
repeat_until: {
repeat: {
until: [],
sequence: [],
},
},
repeat_for_each: {
repeat: {
for_each: {},
sequence: [],
},
},
} as const;
export interface AddAutomationElementDialogParams {
type: "trigger" | "condition" | "action";
add: (key: string) => void;
clipboardItem: string | undefined;
group?: string;
}
const loadDialog = () => import("./add-automation-element-dialog");

View File

@@ -9,14 +9,13 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { transform } from "../../../common/decorators/transform";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { stateActive } from "../../../common/entity/state_active";
import { computeStateName } from "../../../common/entity/compute_state_name";
import {
stateColorBrightness,
stateColorCss,
@@ -41,7 +40,6 @@ import type { FrontendLocaleData } from "../../../data/translation";
import type { Themes } from "../../../data/ws-themes";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities";
import { hasAction } from "../common/has-action";
import { createEntityNotFoundWarning } from "../components/hui-warning";
@@ -51,6 +49,8 @@ import type {
LovelaceGridOptions,
} from "../types";
import type { ButtonCardConfig } from "./types";
import { computeCssColor } from "../../../common/color/compute-color";
import { stateActive } from "../../../common/entity/state_active";
export const getEntityDefaultButtonAction = (entityId?: string) =>
entityId && DOMAINS_TOGGLE.has(computeDomain(entityId))
@@ -183,11 +183,9 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
`;
}
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
const name = this._config.show_name
? this._config.name || (stateObj ? computeStateName(stateObj) : "")
: "";
return html`
<ha-card
@@ -197,7 +195,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
role="button"
aria-label=${name}
aria-label=${this._config.name ||
(stateObj ? computeStateName(stateObj) : "")}
tabindex=${ifDefined(
hasAction(this._config.tap_action) ? "0" : undefined
)}

View File

@@ -272,6 +272,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this._config.state_content}
.name=${name}
>
</state-display>
`;

View File

@@ -4,7 +4,6 @@ import {
type EntityNameItem,
} from "../../../../common/entity/compute_entity_name_display";
import type { HomeAssistant } from "../../../../types";
import { ensureArray } from "../../../../common/array/ensure-array";
/**
* Computes the display name for an entity in Lovelace (cards and badges).
@@ -16,24 +15,9 @@ import { ensureArray } from "../../../../common/array/ensure-array";
*/
export const computeLovelaceEntityName = (
hass: HomeAssistant,
stateObj: HassEntity | undefined,
stateObj: HassEntity,
nameConfig: string | EntityNameItem | EntityNameItem[] | undefined
): string => {
if (typeof nameConfig === "string") {
return nameConfig;
}
const config = nameConfig || DEFAULT_ENTITY_NAME;
if (stateObj) {
return hass.formatEntityName(stateObj, config);
}
// If entity is not found, fall back to text parts in config
// This allows for static names even when the entity is missing
// e.g. for a card that doesn't require an entity
const textParts = ensureArray(config)
.filter((item) => item.type === "text")
.map((item) => ("text" in item ? item.text : ""));
if (textParts.length) {
return textParts.join(" ");
}
return "";
};
): string =>
typeof nameConfig === "string"
? nameConfig
: hass.formatEntityName(stateObj, nameConfig || DEFAULT_ENTITY_NAME);

View File

@@ -5,7 +5,6 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
@@ -17,14 +16,13 @@ import type { ButtonCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(entityNameStruct),
name: optional(string()),
show_name: optional(boolean()),
icon: optional(string()),
show_icon: optional(boolean()),
@@ -70,13 +68,7 @@ export class HuiButtonCardEditor
(entityId: string | undefined) =>
[
{ name: "entity", selector: { entity: {} } },
{
name: "name",
selector: {
entity_name: { default_name: DEFAULT_ENTITY_NAME },
},
context: { entity: "entity" },
},
{ name: "name", selector: { text: {} } },
{
name: "",
type: "grid",

View File

@@ -13,7 +13,6 @@ import {
union,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
@@ -28,7 +27,6 @@ import type { LovelaceGenericElementEditor } from "../../types";
import "../conditions/ha-card-conditions-editor";
import { configElementStyle } from "../config-elements/config-elements-style";
import { actionConfigStruct } from "../structs/action-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = {
type: "entity",
@@ -39,7 +37,7 @@ export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = {
const entityConfigStruct = object({
type: optional(string()),
entity: optional(string()),
name: optional(entityNameStruct),
name: optional(string()),
icon: optional(string()),
state_content: optional(union([string(), array(string())])),
show_state: optional(boolean()),
@@ -94,11 +92,8 @@ export class HuiHeadingEntityEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
text: {},
},
context: { entity: "entity" },
},
{
name: "icon",

View File

@@ -7,6 +7,7 @@ import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-heading-badge";
@@ -15,7 +16,6 @@ import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import "../../../state-display/state-display";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { DEFAULT_CONFIG } from "../editor/heading-badge-editor/hui-entity-heading-badge-editor";
@@ -137,11 +137,7 @@ export class HuiEntityHeadingBadge
"--icon-color": color,
};
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
const name = config.name || computeStateName(stateObj);
return html`
<ha-heading-badge
@@ -170,7 +166,7 @@ export class HuiEntityHeadingBadge
.hass=${this.hass}
.stateObj=${stateObj}
.content=${config.state_content}
.name=${name}
.name=${config.name}
dash-unavailable
></state-display>
`

View File

@@ -155,7 +155,6 @@ export const semanticColorStyles = css`
/* Surfaces */
--ha-color-surface-default: var(--ha-color-neutral-95);
--ha-color-on-surface-default: var(--ha-color-neutral-05);
}
`;
@@ -287,6 +286,5 @@ export const darkSemanticColorStyles = css`
/* Surfaces */
--ha-color-surface-default: var(--ha-color-neutral-10);
--ha-color-on-surface-default: var(--ha-color-neutral-95);
}
`;

View File

@@ -5,6 +5,7 @@ import { customElement, property } from "lit/decorators";
import { join } from "lit/directives/join";
import { ensureArray } from "../common/array/ensure-array";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import "../components/ha-relative-time";
import { isUnavailableState } from "../data/entity";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../data/sensor";
@@ -99,8 +100,8 @@ class StateDisplay extends LitElement {
return this.hass!.formatEntityState(stateObj);
}
if (content === "name" && this.name) {
return html`${this.name}`;
if (content === "name") {
return html`${this.name || computeStateName(stateObj)}`;
}
let relativeDateTime: string | Date | undefined;

View File

@@ -3915,7 +3915,6 @@
"edit_yaml": "Edit in YAML",
"edit_ui": "Edit in visual editor",
"copy_to_clipboard": "Copy to clipboard",
"search_in": "Search · {group}",
"unknown_entity": "unknown entity",
"edit_unknown_device": "Editor not available for unknown device",
"switch_ui_yaml_error": "There are currently YAML errors in the automation, and it cannot be parsed. Switching to UI mode may cause pending changes to be lost. Press cancel to correct any errors before proceeding to prevent loss of pending changes, or continue if you are sure.",
@@ -3928,6 +3927,7 @@
"item_pasted": "{item} pasted",
"ctrl": "Ctrl",
"del": "Del",
"blocks": "Blocks",
"triggers": {
"name": "Triggers",
"header": "When",
@@ -3935,7 +3935,7 @@
"learn_more": "Learn more about triggers",
"triggered": "Triggered",
"add": "Add trigger",
"search": "Search trigger",
"empty_search": "No triggers found for {term}",
"id": "Trigger ID",
"id_helper": "Helps identify each run based on which trigger fired.",
"optional": "Optional",
@@ -3956,14 +3956,16 @@
"trigger": "Trigger",
"copied_to_clipboard": "Trigger copied to clipboard",
"cut_to_clipboard": "Trigger cut to clipboard",
"select": "Select a trigger",
"groups": {
"device": {
"label": "Device"
},
"entity": {
"label": "Entity",
"description": "When something happens to an entity."
"label": "Entity"
},
"time_location": {
"label": "Time and location",
"description": "When someone enters or leaves a zone, or at a specific time."
"label": "Time and location"
},
"other": {
"label": "Other triggers"
@@ -4196,7 +4198,7 @@
"description": "All conditions added here need to be satisfied for the automation to run. A condition can be satisfied or not at any given time, for example: ''If {user} is home''. You can use building blocks to create more complex conditions.",
"learn_more": "Learn more about conditions",
"add": "Add condition",
"search": "Search condition",
"empty_search": "No conditions and blocks found for {term}",
"add_building_block": "Add building block",
"test": "Test",
"testing_error": "Condition did not pass",
@@ -4218,21 +4220,22 @@
"condition": "Condition",
"copied_to_clipboard": "Condition copied to clipboard",
"cut_to_clipboard": "Condition cut to clipboard",
"select": "Select a condition",
"groups": {
"device": {
"label": "Device"
},
"entity": {
"label": "Entity",
"description": "If an entity is in a specific state."
"label": "Entity"
},
"time_location": {
"label": "Time and location",
"description": "If someone is in a zone or if the current time is before or after a specified time."
"label": "Time and location"
},
"other": {
"label": "Other conditions"
},
"building_blocks": {
"label": "Building blocks",
"description": "Build more complex conditions."
"label": "Building blocks"
}
},
"type": {
@@ -4363,7 +4366,7 @@
"description": "All actions added here will be performed in sequence when the automation runs. An action usually controls one of your areas, devices, or entities, for example: 'Turn on the lights'. You can use building blocks to create more complex sequences of actions.",
"learn_more": "Learn more about actions",
"add": "Add action",
"search": "Search action",
"empty_search": "No actions and blocks found for {term}",
"add_building_block": "Add building block",
"invalid_action": "Invalid action",
"run": "Run action",
@@ -4387,7 +4390,11 @@
"action": "Action",
"copied_to_clipboard": "Action copied to clipboard",
"cut_to_clipboard": "Action cut to clipboard",
"select": "Select an action",
"groups": {
"device_id": {
"label": "Device"
},
"helpers": {
"label": "Helpers"
},
@@ -4395,8 +4402,7 @@
"label": "Other actions"
},
"building_blocks": {
"label": "Building blocks",
"description": "Build more complex sequences of actions."
"label": "Building blocks"
}
},
"type": {

View File

@@ -77,71 +77,4 @@ describe("computeLovelaceEntityName", () => {
expect(mockFormatEntityName).toHaveBeenCalledTimes(1);
expect(mockFormatEntityName).toHaveBeenCalledWith(stateObj, nameConfig);
});
describe("when stateObj is undefined", () => {
it("returns empty string when nameConfig is undefined", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const result = computeLovelaceEntityName(hass, undefined, undefined);
expect(result).toBe("");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns text from single text EntityNameItem", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const nameConfig = { type: "text" as const, text: "Custom Text" };
const result = computeLovelaceEntityName(hass, undefined, nameConfig);
expect(result).toBe("Custom Text");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns joined text from multiple text EntityNameItems", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const nameConfig = [
{ type: "text" as const, text: "First" },
{ type: "text" as const, text: "Second" },
];
const result = computeLovelaceEntityName(hass, undefined, nameConfig);
expect(result).toBe("First Second");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns only text items when mixed with non-text items", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const nameConfig = [
{ type: "text" as const, text: "Prefix" },
{ type: "device" as const },
{ type: "text" as const, text: "Suffix" },
{ type: "entity" as const },
];
const result = computeLovelaceEntityName(hass, undefined, nameConfig);
expect(result).toBe("Prefix Suffix");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns empty string when no text items in config", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const nameConfig = [
{ type: "device" as const },
{ type: "entity" as const },
];
const result = computeLovelaceEntityName(hass, undefined, nameConfig);
expect(result).toBe("");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
});
});