Compare commits

...

14 Commits

Author SHA1 Message Date
Paul Bottein 67166be0d7 Add icon 2023-09-29 15:44:08 +02:00
Paul Bottein 4b386c0661 Add current state 2023-09-29 12:45:03 +02:00
Paul Bottein 3a03abbd3a Add "Add condition" button in the editor 2023-09-29 11:17:00 +02:00
Paul Bottein 873ce2b405 Improve performance and editor 2023-09-29 11:17:00 +02:00
Paul Bottein 89b53a76f0 Add reponsive editor with breakpoints 2023-09-29 11:16:59 +02:00
Paul Bottein 15522d4926 Use media query, remove ui editor for responsive 2023-09-29 11:16:59 +02:00
Paul Bottein c6e6255d19 Add UI for responsive condition 2023-09-29 11:16:59 +02:00
Paul Bottein 8bcd730501 Migrate state condition ui to ha-form 2023-09-29 11:16:59 +02:00
Paul Bottein 689cd3d145 Add responsive condition for conditional card 2023-09-29 11:16:59 +02:00
Bram Kragten 8daff17d6a Change linear-progress bar background color for dark mode (#18059) 2023-09-29 10:33:26 +02:00
renovate[bot] 59bd852e7a Update dependency @octokit/rest to v20.0.2 (#18053)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-28 22:51:10 -04:00
renovate[bot] 246fe2861e Update dependency eslint-plugin-wc to v2.0.4 (#18050)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-28 14:18:22 -04:00
renovate[bot] dbf623ada2 Update typescript-eslint monorepo to v6.7.3 (#18051)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-28 14:17:33 -04:00
renovate[bot] c848356a6d Update dependency @types/sortablejs to v1.15.3 (#18049)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-28 14:16:08 -04:00
12 changed files with 845 additions and 229 deletions
+5 -5
View File
@@ -163,7 +163,7 @@
"@lokalise/node-api": "12.0.0",
"@octokit/auth-oauth-device": "6.0.1",
"@octokit/plugin-retry": "6.0.1",
"@octokit/rest": "20.0.1",
"@octokit/rest": "20.0.2",
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.3",
"@rollup/plugin-commonjs": "25.0.4",
@@ -183,12 +183,12 @@
"@types/mocha": "10.0.1",
"@types/qrcode": "1.5.2",
"@types/serve-handler": "6.1.2",
"@types/sortablejs": "1.15.2",
"@types/sortablejs": "1.15.3",
"@types/tar": "6.1.6",
"@types/ua-parser-js": "0.7.37",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.7.2",
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
@@ -205,7 +205,7 @@
"eslint-plugin-lit": "1.9.1",
"eslint-plugin-lit-a11y": "4.1.0",
"eslint-plugin-unused-imports": "3.0.0",
"eslint-plugin-wc": "2.0.3",
"eslint-plugin-wc": "2.0.4",
"esprima": "4.0.1",
"fancy-log": "2.0.0",
"fs-extra": "11.1.1",
+9
View File
@@ -0,0 +1,9 @@
export function getAllCombinations<T>(arr: T[]) {
return arr.reduce<T[][]>(
(combinations, element) =>
combinations.concat(
combinations.map((combination) => [...combination, element])
),
[[]]
);
}
@@ -1,10 +1,44 @@
import { UNAVAILABLE } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
export interface Condition {
entity: string;
export type Condition = StateCondition | ResponsiveCondition;
export type LegacyCondition = {
entity?: string;
state?: string;
state_not?: string;
};
export type StateCondition = {
condition: "state";
entity?: string;
state?: string;
state_not?: string;
};
export type ResponsiveCondition = {
condition: "responsive";
media_query?: string;
};
function checkStateCondition(condition: StateCondition, hass: HomeAssistant) {
const state =
condition.entity && hass.states[condition.entity]
? hass.states[condition.entity].state
: UNAVAILABLE;
return condition.state != null
? state === condition.state
: state !== condition.state_not;
}
function checkResponsiveCondition(
condition: ResponsiveCondition,
_hass: HomeAssistant
) {
return condition.media_query
? matchMedia(condition.media_query).matches
: false;
}
export function checkConditionsMet(
@@ -12,18 +46,30 @@ export function checkConditionsMet(
hass: HomeAssistant
): boolean {
return conditions.every((c) => {
const state = hass.states[c.entity]
? hass!.states[c.entity].state
: UNAVAILABLE;
if (c.condition === "responsive") {
return checkResponsiveCondition(c, hass);
}
return c.state != null ? state === c.state : state !== c.state_not;
return checkStateCondition(c, hass);
});
}
export function validateConditionalConfig(conditions: Condition[]): boolean {
return conditions.every(
(c) =>
(c.entity &&
(c.state != null || c.state_not != null)) as unknown as boolean
function valideStateCondition(condition: StateCondition) {
return (
condition.entity != null &&
(condition.state != null || condition.state_not != null)
);
}
function valideResponsiveCondition(condition: ResponsiveCondition) {
return condition.media_query != null;
}
export function validateConditionalConfig(conditions: Condition[]): boolean {
return conditions.every((c) => {
if (c.condition === "responsive") {
return valideResponsiveCondition(c);
}
return valideStateCondition(c);
});
}
@@ -3,11 +3,14 @@ import { customElement, property } from "lit/decorators";
import { HomeAssistant } from "../../../types";
import { ConditionalCardConfig } from "../cards/types";
import {
ResponsiveCondition,
checkConditionsMet,
validateConditionalConfig,
} from "../common/validate-condition";
import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types";
import { LovelaceCard } from "../types";
import { listenMediaQuery } from "../../../common/dom/media_query";
import { deepEqual } from "../../../common/util/deep-equal";
@customElement("hui-conditional-base")
export class HuiConditionalBase extends ReactiveElement {
@@ -21,6 +24,10 @@ export class HuiConditionalBase extends ReactiveElement {
protected _element?: LovelaceCard | LovelaceRow;
private _mediaQueriesListeners: Array<() => void> = [];
private _mediaQueries: string[] = [];
protected createRenderRoot() {
return this;
}
@@ -47,27 +54,98 @@ export class HuiConditionalBase extends ReactiveElement {
this._config = config;
}
public disconnectedCallback() {
super.disconnectedCallback();
this._clearMediaQueries();
}
public connectedCallback() {
super.connectedCallback();
this._listenMediaQueries();
this._updateVisibility();
}
private _clearMediaQueries() {
this._mediaQueries = [];
while (this._mediaQueriesListeners.length) {
this._mediaQueriesListeners.pop()!();
}
}
private _listenMediaQueries() {
if (!this._config) {
return;
}
const conditions = this._config.conditions.filter(
(c) => c.condition === "responsive"
) as ResponsiveCondition[];
const mediaQueries = conditions
.filter((c) => c.media_query)
.map((c) => c.media_query as string);
if (deepEqual(mediaQueries, this._mediaQueries)) return;
this._mediaQueries = mediaQueries;
while (this._mediaQueriesListeners.length) {
this._mediaQueriesListeners.pop()!();
}
mediaQueries.forEach((query) => {
const listener = listenMediaQuery(query, (matches) => {
// For performance, if there is only one condition, set the visibility directly
if (this._config!.conditions.length === 1) {
this._setVisibility(matches);
return;
}
this._updateVisibility();
});
this._mediaQueriesListeners.push(listener);
});
}
protected update(changed: PropertyValues): void {
super.update(changed);
if (
changed.has("_element") ||
changed.has("_config") ||
changed.has("hass")
) {
this._listenMediaQueries();
this._updateVisibility();
}
}
private _updateVisibility() {
if (!this._element || !this.hass || !this._config) {
return;
}
this._element.editMode = this.editMode;
const visible =
this.editMode || checkConditionsMet(this._config.conditions, this.hass);
this.hidden = !visible;
const conditionMet = checkConditionsMet(
this._config!.conditions,
this.hass!
);
this._setVisibility(conditionMet);
}
private _setVisibility(conditionMet: boolean) {
if (!this._element || !this.hass) {
return;
}
const visible = this.editMode || conditionMet;
this.hidden = !visible;
this.style.setProperty("display", visible ? "" : "none");
if (visible) {
this._element.hass = this.hass;
if (!this._element.parentElement) {
this.appendChild(this._element);
if (!this._element!.parentElement) {
this.appendChild(this._element!);
}
} else if (this._element.parentElement) {
this.removeChild(this._element);
this.removeChild(this._element!);
}
}
}
@@ -0,0 +1,128 @@
import { mdiCodeBraces, mdiDelete, mdiListBoxOutline } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-yaml-editor";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { Condition, LegacyCondition } from "../../common/validate-condition";
import type { LovelaceConditionEditorConstructor } from "./types";
@customElement("ha-card-condition-editor")
export default class HaCardConditionEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) condition!: Condition | LegacyCondition;
@state() public _yamlMode = false;
protected render() {
const condition: Condition = {
condition: "state",
...this.condition,
};
const element = customElements.get(
`ha-card-condition-${condition.condition}`
) as LovelaceConditionEditorConstructor | undefined;
const supported = element !== undefined;
const valid =
element &&
(!element.validateUIConfig || element.validateUIConfig(condition));
const yamlMode = this._yamlMode || !supported || !valid;
return html`
<div class="header">
<ha-icon-button
@click=${this._toggleMode}
.disabled=${!supported || !valid}
.label=${this.hass!.localize(
yamlMode
? "ui.panel.lovelace.editor.edit_card.show_visual_editor"
: "ui.panel.lovelace.editor.edit_card.show_code_editor"
)}
.path=${yamlMode ? mdiListBoxOutline : mdiCodeBraces}
></ha-icon-button>
<span class="title">
${this.hass.localize(
`ui.panel.lovelace.editor.card.conditional.condition.${condition.condition}.label`
) || condition.condition}
</span>
<ha-icon-button
.label=${this.hass!.localize("ui.common.delete")}
.path=${mdiDelete}
@click=${this._delete}
>
</ha-icon-button>
</div>
${!valid
? html`
<ha-alert alert-type="warning">
${this.hass.localize("ui.errors.config.editor_not_supported")}
</ha-alert>
`
: nothing}
<div class="content">
${yamlMode
? html`
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.condition}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
${dynamicElement(`ha-card-condition-${condition.condition}`, {
hass: this.hass,
condition: condition,
})}
`}
</div>
`;
}
private _toggleMode() {
this._yamlMode = !this._yamlMode;
}
private _delete() {
fireEvent(this, "value-changed", { value: null });
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
}
// @ts-ignore
fireEvent(this, "value-changed", { value: ev.detail.value });
}
static styles = [
haStyle,
css`
.header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.header span {
flex: 1;
font-size: 16px;
}
.content {
padding: 12px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-card-condition-editor": HaCardConditionEditor;
}
}
@@ -0,0 +1,6 @@
import { Condition } from "../../common/validate-condition";
export interface LovelaceConditionEditorConstructor {
defaultConfig?: Condition;
validateUIConfig?: (condition: Condition) => boolean;
}
@@ -0,0 +1,196 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { getAllCombinations } from "../../../../../common/array/combinations";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import { HaFormSchema } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import { ResponsiveCondition } from "../../../common/validate-condition";
const BREAKPOINT_VALUES = [0, 768, 1024, 1280, Infinity];
const BREAKPOINTS = ["mobile", "tablet", "desktop", "wide"] as const;
type BreakpointSize = [number, number];
type Breakpoint = (typeof BREAKPOINTS)[number];
function mergeConsecutiveRanges(arr: [number, number][]): [number, number][] {
if (arr.length === 0) {
return [];
}
[...arr].sort((a, b) => a[0] - b[0]);
const mergedRanges = [arr[0]];
for (let i = 1; i < arr.length; i++) {
const currentRange = arr[i];
const previousRange = mergedRanges[mergedRanges.length - 1];
if (currentRange[0] <= previousRange[1] + 1) {
previousRange[1] = currentRange[1];
} else {
mergedRanges.push(currentRange);
}
}
return mergedRanges;
}
function buildMediaQuery(size: BreakpointSize) {
const [min, max] = size;
const query: string[] = [];
if (min != null) {
query.push(`(min-width: ${min}px)`);
}
if (max != null && max !== Infinity) {
query.push(`(max-width: ${max - 1}px)`);
}
return query.join(" and ");
}
function computeBreakpointsSize(breakpoints: Breakpoint[]) {
const sizes = breakpoints.map<BreakpointSize>((breakpoint) => {
const index = BREAKPOINTS.indexOf(breakpoint);
return [BREAKPOINT_VALUES[index], BREAKPOINT_VALUES[index + 1] || Infinity];
});
const mergedSizes = mergeConsecutiveRanges(sizes);
const queries = mergedSizes
.map((size) => buildMediaQuery(size))
.filter((size) => size);
return queries.join(", ");
}
function computeBreakpointsKey(breakpoints) {
return [...breakpoints].sort().join("_");
}
// Compute all possible media queries from each breakpoints combination (2 ^ breakpoints = 16)
const queries = getAllCombinations(BREAKPOINTS as unknown as Breakpoint[])
.filter((arr) => arr.length !== 0)
.map(
(breakpoints) =>
[breakpoints, computeBreakpointsSize(breakpoints)] as [
Breakpoint[],
string,
]
);
// Store them in maps to avoid recomputing them
const mediaQueryMap = new Map(
queries.map(([b, m]) => [computeBreakpointsKey(b), m])
);
const mediaQueryReverseMap = new Map(queries.map(([b, m]) => [m, b]));
type ResponsiveConditionData = {
breakpoints: Breakpoint[];
};
@customElement("ha-card-condition-responsive")
export class HaCardConditionResponsive extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public condition!: ResponsiveCondition;
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): ResponsiveCondition {
return { condition: "responsive", media_query: "" };
}
protected static validateUIConfig(condition: ResponsiveCondition) {
return (
!condition.media_query || mediaQueryReverseMap.get(condition.media_query)
);
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{
name: "breakpoints",
selector: {
select: {
mode: "list",
options: BREAKPOINTS.map((b) => {
const value = BREAKPOINT_VALUES[BREAKPOINTS.indexOf(b)];
return {
value: b,
label: `${localize(
`ui.panel.lovelace.editor.card.conditional.condition.responsive.breakpoints_list.${b}`
)}${
value
? ` (${localize(
`ui.panel.lovelace.editor.card.conditional.condition.responsive.min`,
{ size: value }
)})`
: ""
}`,
};
}),
multiple: true,
},
},
},
] as const satisfies readonly HaFormSchema[]
);
protected render() {
const breakpoints = this.condition.media_query
? mediaQueryReverseMap.get(this.condition.media_query)
: undefined;
const data: ResponsiveConditionData = {
breakpoints: breakpoints ?? [],
};
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${this._schema(this.hass.localize)}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabelCallback}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const data = ev.detail.value as ResponsiveConditionData;
const { breakpoints } = data;
const condition: ResponsiveCondition = {
condition: "responsive",
media_query: mediaQueryMap.get(computeBreakpointsKey(breakpoints)) ?? "",
};
fireEvent(this, "value-changed", { value: condition });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (schema.name) {
case "breakpoints":
return this.hass.localize(
`ui.panel.lovelace.editor.card.conditional.condition.responsive.${schema.name}`
);
default:
return "";
}
};
}
declare global {
interface HTMLElementTagNameMap {
"ha-card-condition-responsive": HaCardConditionResponsive;
}
}
@@ -0,0 +1,164 @@
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { assert, literal, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import { HaFormSchema } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import { StateCondition } from "../../../common/validate-condition";
const stateConditionStruct = object({
condition: literal("state"),
entity: string(),
state: optional(string()),
state_not: optional(string()),
});
type StateConditionData = {
condition: "state";
entity: string;
invert: "true" | "false";
state?: string;
};
@customElement("ha-card-condition-state")
export class HaCardConditionState extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public condition!: StateCondition;
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): StateCondition {
return { condition: "state", entity: "", state: "" };
}
protected willUpdate(changedProperties: PropertyValues): void {
if (!changedProperties.has("condition")) {
return;
}
try {
assert(this.condition, stateConditionStruct);
} catch (err: any) {
fireEvent(this, "ui-mode-not-available", err);
}
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{ name: "entity", selector: { entity: {} } },
{
name: "",
type: "grid",
schema: [
{
name: "invert",
selector: {
select: {
mode: "dropdown",
options: [
{
label: localize(
"ui.panel.lovelace.editor.card.conditional.state_equal"
),
value: "false",
},
{
label: localize(
"ui.panel.lovelace.editor.card.conditional.state_not_equal"
),
value: "true",
},
],
},
},
},
{
name: "state",
selector: {
state: {},
},
context: {
filter_entity: "entity",
},
},
],
},
] as const satisfies readonly HaFormSchema[]
);
protected render() {
const { state, state_not, ...content } = this.condition;
const data: StateConditionData = {
...content,
entity: this.condition.entity ?? "",
invert: this.condition.state_not ? "true" : "false",
state: this.condition.state_not ?? this.condition.state ?? "",
};
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${this._schema(this.hass.localize)}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabelCallback}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const data = ev.detail.value as StateConditionData;
const { invert, state, entity, condition: _, ...content } = data;
const condition: StateCondition = {
condition: "state",
...content,
entity: entity ?? "",
state: invert === "false" ? state ?? "" : undefined,
state_not: invert === "true" ? state ?? "" : undefined,
};
fireEvent(this, "value-changed", { value: condition });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
const entity = this.condition.entity
? this.hass.states[this.condition.entity]
: undefined;
switch (schema.name) {
case "entity":
return this.hass.localize("ui.components.entity.entity-picker.entity");
case "state":
if (entity) {
return `${this.hass.localize(
"ui.components.entity.entity-state-picker.state"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.conditional.current_state"
)}: ${this.hass.formatEntityState(entity)})`;
}
return `${this.hass.localize(
"ui.components.entity.entity-state-picker.state"
)}`;
default:
return "";
}
};
}
declare global {
interface HTMLElementTagNameMap {
"ha-card-condition-state": HaCardConditionState;
}
}
@@ -1,55 +1,66 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab";
import { mdiCodeBraces, mdiContentCopy, mdiListBoxOutline } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { MDCTabBarActivatedEvent } from "@material/tab-bar";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import {
any,
array,
assert,
assign,
object,
optional,
string,
} from "superstruct";
mdiCodeBraces,
mdiContentCopy,
mdiListBoxOutline,
mdiPlus,
mdiResponsive,
mdiStateMachine,
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { any, array, assert, assign, object, optional } from "superstruct";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-select";
import "../../../../components/ha-textfield";
import "../../../../components/ha-button";
import "../../../../components/ha-list-item";
import "../../../../components/ha-menu-button";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon";
import type {
LovelaceCardConfig,
LovelaceConfig,
} from "../../../../data/lovelace";
import type { HomeAssistant } from "../../../../types";
import type { ConditionalCardConfig } from "../../cards/types";
import { Condition } from "../../common/validate-condition";
import type { LovelaceCardEditor } from "../../types";
import "../card-editor/hui-card-element-editor";
import type { HuiCardElementEditor } from "../card-editor/hui-card-element-editor";
import "../card-editor/hui-card-picker";
import "../conditions/ha-card-condition-editor";
import { LovelaceConditionEditorConstructor } from "../conditions/types";
import "../conditions/types/ha-card-condition-responsive";
import "../conditions/types/ha-card-condition-state";
import "../hui-element-editor";
import type { ConfigChangedEvent } from "../hui-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { GUIModeChangedEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
const conditionStruct = object({
entity: string(),
state: optional(string()),
state_not: optional(string()),
});
const UI_CONDITION = [
"state",
"responsive",
] as const satisfies readonly Condition["condition"][];
type UiCondition = (typeof UI_CONDITION)[number];
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
card: any(),
conditions: optional(array(conditionStruct)),
conditions: optional(array(any())),
})
);
const ICONS: Record<UiCondition, string> = {
state: mdiStateMachine,
responsive: mdiResponsive,
};
@customElement("hui-conditional-card-editor")
export class HuiConditionalCardEditor
extends LitElement
@@ -127,7 +138,6 @@ export class HuiConditionalCardEditor
)}
.path=${isGuiMode ? mdiCodeBraces : mdiListBoxOutline}
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.copy"
@@ -166,61 +176,44 @@ export class HuiConditionalCardEditor
${this._config.conditions.map(
(cond, idx) => html`
<div class="condition">
<div class="entity">
<ha-entity-picker
.hass=${this.hass}
.value=${cond.entity}
.idx=${idx}
.configValue=${"entity"}
@change=${this._changeCondition}
allow-custom-entity
></ha-entity-picker>
</div>
<div class="state">
<ha-select
.value=${cond.state_not !== undefined
? "true"
: "false"}
.idx=${idx}
.configValue=${"invert"}
@selected=${this._changeCondition}
@closed=${stopPropagation}
naturalMenuWidth
fixedMenuPosition
>
<mwc-list-item value="false">
${this.hass!.localize(
"ui.panel.lovelace.editor.card.conditional.state_equal"
)}
</mwc-list-item>
<mwc-list-item value="true">
${this.hass!.localize(
"ui.panel.lovelace.editor.card.conditional.state_not_equal"
)}
</mwc-list-item>
</ha-select>
<ha-textfield
.label="${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.state"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.conditional.current_state"
)}: ${this.hass?.states[cond.entity].state})"
.value=${cond.state_not !== undefined
? cond.state_not
: cond.state}
.idx=${idx}
.configValue=${"state"}
@input=${this._changeCondition}
></ha-textfield>
</div>
<ha-card-condition-editor
.index=${idx}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
.condition=${cond}
></ha-card-condition-editor>
</div>
`
)}
<div class="condition">
<ha-entity-picker
.hass=${this.hass}
@change=${this._addCondition}
></ha-entity-picker>
<div>
<ha-button-menu
@action=${this._addCondition}
fixed
@closed=${stopPropagation}
>
<ha-button
slot="trigger"
outlined
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.conditional.add_condition"
)}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
${UI_CONDITION.map(
(condition) => html`
<ha-list-item .value=${condition} graphic="icon">
${this.hass!.localize(
`ui.panel.lovelace.editor.card.conditional.condition.${condition}.label`
) || condition}
<ha-svg-icon
slot="graphic"
.path=${ICONS[condition]}
></ha-svg-icon>
</ha-list-item>
`
)}
</ha-button-menu>
</div>
</div>
`}
@@ -289,53 +282,40 @@ export class HuiConditionalCardEditor
fireEvent(this, "config-changed", { config: this._config });
}
private _addCondition(ev: Event): void {
const target = ev.target! as any;
if (target.value === "" || !this._config) {
private _addCondition(ev: CustomEvent): void {
const condition = (ev.currentTarget as HaSelect).items[ev.detail.index]
.value as Condition["condition"];
if (!this._config) {
return;
}
const conditions = [...this._config.conditions];
conditions.push({
entity: target.value,
state: "",
});
const elClass = customElements.get(`ha-card-condition-${condition}`) as
| LovelaceConditionEditorConstructor
| undefined;
conditions.push(
elClass?.defaultConfig
? { ...elClass.defaultConfig }
: { condition: condition }
);
this._config = { ...this._config, conditions };
target.value = "";
fireEvent(this, "config-changed", { config: this._config });
}
private _changeCondition(ev: Event): void {
const target = ev.target as any;
if (!this._config || !target) {
return;
}
const conditions = [...this._config.conditions];
if (target.configValue === "entity" && target.value === "") {
conditions.splice(target.idx, 1);
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
const conditions = [...this._config!.conditions];
const newValue = ev.detail.value;
const index = (ev.target as any).index;
if (newValue === null) {
conditions.splice(index, 1);
} else {
const condition = { ...conditions[target.idx] };
if (target.configValue === "entity") {
condition.entity = target.value;
} else if (target.configValue === "state") {
if (condition.state_not !== undefined) {
condition.state_not = target.value;
} else {
condition.state = target.value;
}
} else if (target.configValue === "invert") {
if (target.value === "true") {
if (condition.state) {
condition.state_not = condition.state;
delete condition.state;
}
} else if (condition.state_not) {
condition.state = condition.state_not;
delete condition.state_not;
}
}
conditions[target.idx] = condition;
conditions[index] = newValue;
}
this._config = { ...this._config, conditions };
this._config = { ...this._config!, conditions };
fireEvent(this, "config-changed", { config: this._config });
}
@@ -352,22 +332,13 @@ export class HuiConditionalCardEditor
.condition {
margin-top: 8px;
border: 1px solid var(--divider-color);
}
.condition .content {
padding: 12px;
}
.condition .state {
display: flex;
align-items: flex-end;
ha-button-menu {
margin-top: 12px;
}
.condition .state ha-select {
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
direction: var(--direction);
}
.condition .state ha-textfield {
flex-grow: 1;
}
.card {
margin-top: 8px;
border: 1px solid var(--divider-color);
+1
View File
@@ -14,6 +14,7 @@ export const darkStyles = {
"switch-unchecked-track-color": "#9b9b9b",
"divider-color": "rgba(225, 225, 225, .12)",
"mdc-ripple-color": "#AAAAAA",
"mdc-linear-progress-buffer-color": "rgba(255, 255, 255, 0.1)",
"input-idle-line-color": "rgba(255, 255, 255, 0.42)",
"input-hover-line-color": "rgba(255, 255, 255, 0.87)",
+18 -1
View File
@@ -4768,7 +4768,24 @@
"state_not_equal": "State is not equal to",
"current_state": "current",
"condition_explanation": "The card will be shown when ALL conditions below are fulfilled.",
"change_type": "Change type"
"change_type": "Change type",
"add_condition": "Add condition",
"condition": {
"responsive": {
"label": "Responsive",
"breakpoints": "Screen sizes",
"breakpoints_list": {
"mobile": "Mobile",
"tablet": "Tablet",
"desktop": "Desktop",
"wide": "Wide"
},
"min": "min: {size}px"
},
"state": {
"label": "Entity state"
}
}
},
"config": {
"required": "required",
+79 -79
View File
@@ -3314,14 +3314,14 @@ __metadata:
languageName: node
linkType: hard
"@octokit/plugin-paginate-rest@npm:^8.0.0":
version: 8.0.0
resolution: "@octokit/plugin-paginate-rest@npm:8.0.0"
"@octokit/plugin-paginate-rest@npm:^9.0.0":
version: 9.0.0
resolution: "@octokit/plugin-paginate-rest@npm:9.0.0"
dependencies:
"@octokit/types": ^11.0.0
"@octokit/types": ^12.0.0
peerDependencies:
"@octokit/core": ">=5"
checksum: b5d7cee50523862c6ce7be057f7200e14ee4dcded462f27304c822c960a37efa23ed51080ea879f5d1e56e78f74baa17d2ce32eed5d726794abc35755777e32c
checksum: 4a8543f3e45c4916f94edc57d66106ee60da9fd4edccd7c3a2ddd00da1fc4eb2e1b2bcb3d6cb981a050edf883100c5004d0b81497568d4ac9138310a2188a458
languageName: node
linkType: hard
@@ -3334,14 +3334,14 @@ __metadata:
languageName: node
linkType: hard
"@octokit/plugin-rest-endpoint-methods@npm:^9.0.0":
version: 9.0.0
resolution: "@octokit/plugin-rest-endpoint-methods@npm:9.0.0"
"@octokit/plugin-rest-endpoint-methods@npm:^10.0.0":
version: 10.0.0
resolution: "@octokit/plugin-rest-endpoint-methods@npm:10.0.0"
dependencies:
"@octokit/types": ^11.0.0
"@octokit/types": ^12.0.0
peerDependencies:
"@octokit/core": ">=5"
checksum: 8795cb29be042c839098886a03c2ec6051e3fd7a29f16f4f8a487aa2d85ceb00df8a4432499a43af550369bd730ce9b1b9d7eeff768745b80a3e67698ca9a5dd
checksum: 96b14346997d32092da72d8f297fc3f4ea6cdc5d77a645684fc772ead8d5da41ffc8d45752875c014b71451793f514b23ddb41e1c0caf9bd942c2ea0a75582c4
languageName: node
linkType: hard
@@ -3382,15 +3382,15 @@ __metadata:
languageName: node
linkType: hard
"@octokit/rest@npm:20.0.1":
version: 20.0.1
resolution: "@octokit/rest@npm:20.0.1"
"@octokit/rest@npm:20.0.2":
version: 20.0.2
resolution: "@octokit/rest@npm:20.0.2"
dependencies:
"@octokit/core": ^5.0.0
"@octokit/plugin-paginate-rest": ^8.0.0
"@octokit/plugin-paginate-rest": ^9.0.0
"@octokit/plugin-request-log": ^4.0.0
"@octokit/plugin-rest-endpoint-methods": ^9.0.0
checksum: 9fb2e154a498e00598379b09d76cc7b67b3801e9c97d753f1a76e1163924188bf4cb1411ec152a038ae91e97b86d7146ff220b05adfb6e500e2300c87e14100a
"@octokit/plugin-rest-endpoint-methods": ^10.0.0
checksum: 5c56ac23f063a5375ae5a0609e6477d90afac872eccd07002870f9dd62755037282a74c15c90c848b76b64ea4b5af52f32720b8a1b7b133e758118d11d2c0e34
languageName: node
linkType: hard
@@ -4577,10 +4577,10 @@ __metadata:
languageName: node
linkType: hard
"@types/sortablejs@npm:1.15.2":
version: 1.15.2
resolution: "@types/sortablejs@npm:1.15.2"
checksum: 7e2be1a44efb0df012152f6bd1424dd40a490f90a856b6cf29ffed5fb184cb3fbf61302d0cb0a04f64fe9929ced72eb1c9a1e9b2164e269592130e9f983f54be
"@types/sortablejs@npm:1.15.3":
version: 1.15.3
resolution: "@types/sortablejs@npm:1.15.3"
checksum: 189cc8b16666e27f0de5894731b95889a8be29d8e68d0afdcbd703e1cd519623b2e42049fe66d25e4ec403dffed594b19bb1d16aab6939d0cdfb4d50e2fb99b6
languageName: node
linkType: hard
@@ -4633,15 +4633,15 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:6.7.2":
version: 6.7.2
resolution: "@typescript-eslint/eslint-plugin@npm:6.7.2"
"@typescript-eslint/eslint-plugin@npm:6.7.3":
version: 6.7.3
resolution: "@typescript-eslint/eslint-plugin@npm:6.7.3"
dependencies:
"@eslint-community/regexpp": ^4.5.1
"@typescript-eslint/scope-manager": 6.7.2
"@typescript-eslint/type-utils": 6.7.2
"@typescript-eslint/utils": 6.7.2
"@typescript-eslint/visitor-keys": 6.7.2
"@typescript-eslint/scope-manager": 6.7.3
"@typescript-eslint/type-utils": 6.7.3
"@typescript-eslint/utils": 6.7.3
"@typescript-eslint/visitor-keys": 6.7.3
debug: ^4.3.4
graphemer: ^1.4.0
ignore: ^5.2.4
@@ -4654,44 +4654,44 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: 4d6f612619282a20518cd6581bce16cd7c50ac4e49f5eeca2ab916a923049379aa382817568c929216381fb2c1bfbc1c4e6fde16ac8bfdd63862a9126f0ab797
checksum: ac2790882199047abc59c0407a862f3339645623d03ea0aae5a73fd4bac6abfb753afcf9f23fd51cd1d5aa73f132ef94e2850774c4b2a3d99ebb83030b09429c
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:6.7.2":
version: 6.7.2
resolution: "@typescript-eslint/parser@npm:6.7.2"
"@typescript-eslint/parser@npm:6.7.3":
version: 6.7.3
resolution: "@typescript-eslint/parser@npm:6.7.3"
dependencies:
"@typescript-eslint/scope-manager": 6.7.2
"@typescript-eslint/types": 6.7.2
"@typescript-eslint/typescript-estree": 6.7.2
"@typescript-eslint/visitor-keys": 6.7.2
"@typescript-eslint/scope-manager": 6.7.3
"@typescript-eslint/types": 6.7.3
"@typescript-eslint/typescript-estree": 6.7.3
"@typescript-eslint/visitor-keys": 6.7.3
debug: ^4.3.4
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
peerDependenciesMeta:
typescript:
optional: true
checksum: 9e93d3eb432ed5457a852e25a31782d07518f728966cd477620175ae64db9be04f5d8e605f3561dbfe9a365f209a83b2a3788efb9b3cf33669c8bca17f1bcf6f
checksum: 658f3294b281db06ebb46884b92172d45eb402ec25c7d4a09cc2461eee359266029af7a49eb9006ee7c3e0003ba53a06f4bee84aa2e99d2d9a3507b9c84ff775
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:6.7.2":
version: 6.7.2
resolution: "@typescript-eslint/scope-manager@npm:6.7.2"
"@typescript-eslint/scope-manager@npm:6.7.3":
version: 6.7.3
resolution: "@typescript-eslint/scope-manager@npm:6.7.3"
dependencies:
"@typescript-eslint/types": 6.7.2
"@typescript-eslint/visitor-keys": 6.7.2
checksum: e35fa23ecb16252c3ad00b5f1ec05d9b8d33ee30d4c57543892f900443ed77926be9bd2836f06463c31b483f5f0f79070273bc51c4a606f55ac3cd1d9c9cd542
"@typescript-eslint/types": 6.7.3
"@typescript-eslint/visitor-keys": 6.7.3
checksum: 08215444b7c70af5c45e185ba3c31c550a0a671ab464a67058cbee680c94aa9d1a062958976d8b09f7bcabf2f63114cdc7be2e4e32e2dfdcb2d7cc79961b7b32
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:6.7.2":
version: 6.7.2
resolution: "@typescript-eslint/type-utils@npm:6.7.2"
"@typescript-eslint/type-utils@npm:6.7.3":
version: 6.7.3
resolution: "@typescript-eslint/type-utils@npm:6.7.3"
dependencies:
"@typescript-eslint/typescript-estree": 6.7.2
"@typescript-eslint/utils": 6.7.2
"@typescript-eslint/typescript-estree": 6.7.3
"@typescript-eslint/utils": 6.7.3
debug: ^4.3.4
ts-api-utils: ^1.0.1
peerDependencies:
@@ -4699,23 +4699,23 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: 67743f8e4b77d0ab3d82907eda0411ffd221357b60ac9cbd29683d5b8c77127369ebfafcf0bfc30a1f1828927ccd5635fab5b2eaf2b2f1d12a9361549cab3e62
checksum: f30a5ab4f88f76457810d72e3ada79fefd94dbbb456069ac004bd7601c9b7f15689b906b66cd849c230f30ae65f6f7039fb169609177ab545b34bacab64f015e
languageName: node
linkType: hard
"@typescript-eslint/types@npm:6.7.2":
version: 6.7.2
resolution: "@typescript-eslint/types@npm:6.7.2"
checksum: 5a7c4cd456f721649757d2edb4cae71d1405c1c2c35672031f012b27007b9d49b7118297eec746dc3351370e6aa414e5d2c493fb658c7b910154b7998c0278e1
"@typescript-eslint/types@npm:6.7.3":
version: 6.7.3
resolution: "@typescript-eslint/types@npm:6.7.3"
checksum: 4adb6177ec710e7438610fee553839a7abecc498dbb36d0170786bab66c5e5415cd720ac06419fd905458ad88c39b661603af5f013adc299137ccb4c51c4c879
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:6.7.2":
version: 6.7.2
resolution: "@typescript-eslint/typescript-estree@npm:6.7.2"
"@typescript-eslint/typescript-estree@npm:6.7.3":
version: 6.7.3
resolution: "@typescript-eslint/typescript-estree@npm:6.7.3"
dependencies:
"@typescript-eslint/types": 6.7.2
"@typescript-eslint/visitor-keys": 6.7.2
"@typescript-eslint/types": 6.7.3
"@typescript-eslint/visitor-keys": 6.7.3
debug: ^4.3.4
globby: ^11.1.0
is-glob: ^4.0.3
@@ -4724,34 +4724,34 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: c30b9803567c37527e2806badd98f3083ae125db9a430d8a28647b153e446e6a4b830833f229cca27d5aa0ff5497c149aaa524aa3a6dbf932b557c60d0bfd4f9
checksum: eaba1feb0e6882b0bad292172c118aac43ba683d1f04b940b542a20035468d030b062b036ea49eca36aa21782e9b1019e87717003b3c3db7d12dc707466b7eb7
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:6.7.2":
version: 6.7.2
resolution: "@typescript-eslint/utils@npm:6.7.2"
"@typescript-eslint/utils@npm:6.7.3":
version: 6.7.3
resolution: "@typescript-eslint/utils@npm:6.7.3"
dependencies:
"@eslint-community/eslint-utils": ^4.4.0
"@types/json-schema": ^7.0.12
"@types/semver": ^7.5.0
"@typescript-eslint/scope-manager": 6.7.2
"@typescript-eslint/types": 6.7.2
"@typescript-eslint/typescript-estree": 6.7.2
"@typescript-eslint/scope-manager": 6.7.3
"@typescript-eslint/types": 6.7.3
"@typescript-eslint/typescript-estree": 6.7.3
semver: ^7.5.4
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
checksum: 97f950562dba2bda63ffe64672f643ef940123cf74007bc878afcf31c75f905c99934a3ad77da3d5a4fe7807d5d69c791b20c429712ad5a5525e331ebc313756
checksum: 685b7c9fa95ad085f30e26431dc41b3059a42a16925defe2a94b32fb46974bfc168000de7d4d9ad4a1d0568a983f9d3c01ea6bc6cfa9a798e482719af9e9165b
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:6.7.2":
version: 6.7.2
resolution: "@typescript-eslint/visitor-keys@npm:6.7.2"
"@typescript-eslint/visitor-keys@npm:6.7.3":
version: 6.7.3
resolution: "@typescript-eslint/visitor-keys@npm:6.7.3"
dependencies:
"@typescript-eslint/types": 6.7.2
"@typescript-eslint/types": 6.7.3
eslint-visitor-keys: ^3.4.1
checksum: b4915fbc0f3d44c81b92b7151830b698e8b6ed2dee8587bb65540c888c7a84300d3fd6b0c159e2131c7c6df1bebe49fb0d21c347ecdbf7f3e4aec05acebbb0bc
checksum: cef64173a919107f420703e204d97d0afef0d9bd7a67570df5bdb39ac9464211c5a7b3af735d8f41e8004b443ab83e88b1d6fb951886aed4d3fe9d4778667199
languageName: node
linkType: hard
@@ -8126,15 +8126,15 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-wc@npm:2.0.3":
version: 2.0.3
resolution: "eslint-plugin-wc@npm:2.0.3"
"eslint-plugin-wc@npm:2.0.4":
version: 2.0.4
resolution: "eslint-plugin-wc@npm:2.0.4"
dependencies:
is-valid-element-name: ^1.0.0
js-levenshtein-esm: ^1.2.0
peerDependencies:
eslint: ">=5"
checksum: f1c10ea1eccf63a4bd69a407e5b489355a4c0ca0cc5c804162e6809b94dabffcb5e872fc9337ccb4133b3fc54a57333fb54fb6ba14942b684fa723435e5ab3f3
checksum: 0ffc5fc243bb52cb3268f647fa207a3ce4b8ae251eaad3a6cabf9537a6a35dad64d81372d893aa9acf44ce0f927f5e0adf24bcc9ed1c222b5c017ef49f7016a0
languageName: node
linkType: hard
@@ -9692,7 +9692,7 @@ __metadata:
"@mdi/svg": 7.2.96
"@octokit/auth-oauth-device": 6.0.1
"@octokit/plugin-retry": 6.0.1
"@octokit/rest": 20.0.1
"@octokit/rest": 20.0.2
"@open-wc/dev-server-hmr": 0.1.4
"@polymer/iron-flex-layout": 3.0.1
"@polymer/iron-input": 3.0.1
@@ -9723,12 +9723,12 @@ __metadata:
"@types/mocha": 10.0.1
"@types/qrcode": 1.5.2
"@types/serve-handler": 6.1.2
"@types/sortablejs": 1.15.2
"@types/sortablejs": 1.15.3
"@types/tar": 6.1.6
"@types/ua-parser-js": 0.7.37
"@types/webspeechapi": 0.0.29
"@typescript-eslint/eslint-plugin": 6.7.2
"@typescript-eslint/parser": 6.7.2
"@typescript-eslint/eslint-plugin": 6.7.3
"@typescript-eslint/parser": 6.7.3
"@vaadin/combo-box": 24.1.9
"@vaadin/vaadin-themable-mixin": 24.1.9
"@vibrant/color": 3.2.1-alpha.1
@@ -9762,7 +9762,7 @@ __metadata:
eslint-plugin-lit: 1.9.1
eslint-plugin-lit-a11y: 4.1.0
eslint-plugin-unused-imports: 3.0.0
eslint-plugin-wc: 2.0.3
eslint-plugin-wc: 2.0.4
esprima: 4.0.1
fancy-log: 2.0.0
fs-extra: 11.1.1