From 7cbebfd60309dc5ba2f3181250858894fdf2ace4 Mon Sep 17 00:00:00 2001 From: ildar170975 <71872483+ildar170975@users.noreply.github.com> Date: Wed, 13 Nov 2024 19:05:03 +0300 Subject: [PATCH 01/19] Add outline to a label (2) (#22803) * Update ha-filter-labels.ts * Update ha-automation-picker.ts * Update ha-scene-dashboard.ts * Update ha-script-picker.ts * Update ha-config-devices-dashboard.ts * Update ha-config-entities.ts * Update ha-config-helpers.ts * Update ha-filter-labels.ts * Update ha-automation-picker.ts * Update ha-config-devices-dashboard.ts * Update ha-config-helpers.ts * Update ha-script-picker.ts * Update ha-scene-dashboard.ts * Update ha-config-entities.ts * Update ha-data-table-labels.ts * Update ha-label.ts * Update ha-label.ts --- src/components/data-table/ha-data-table-labels.ts | 1 - src/components/ha-label.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/data-table/ha-data-table-labels.ts b/src/components/data-table/ha-data-table-labels.ts index 31dc5be185..b35656be82 100644 --- a/src/components/data-table/ha-data-table-labels.ts +++ b/src/components/data-table/ha-data-table-labels.ts @@ -113,7 +113,6 @@ class HaDataTableLabels extends LitElement { ha-label { --ha-label-background-color: var(--color, var(--grey-color)); --ha-label-background-opacity: 0.5; - outline: 1px solid var(--outline-color); } ha-button-menu { border-radius: 10px; diff --git a/src/components/ha-label.ts b/src/components/ha-label.ts index 668f28017e..1b15d914ed 100644 --- a/src/components/ha-label.ts +++ b/src/components/ha-label.ts @@ -26,6 +26,7 @@ class HaLabel extends LitElement { 0.15 ); --ha-label-background-opacity: 1; + border: 1px solid var(--outline-color); position: relative; box-sizing: border-box; display: inline-flex; From 6bdc7af09f9f8d2b133d1ab420ab2a98f370cc75 Mon Sep 17 00:00:00 2001 From: ildar170975 <71872483+ildar170975@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:05:01 +0300 Subject: [PATCH 02/19] Add outline to a label (3) - consistency (#22812) Update ha-config-labels.ts --- src/panels/config/labels/ha-config-labels.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/panels/config/labels/ha-config-labels.ts b/src/panels/config/labels/ha-config-labels.ts index 1d974bb7a5..2e52ddb663 100644 --- a/src/panels/config/labels/ha-config-labels.ts +++ b/src/panels/config/labels/ha-config-labels.ts @@ -106,7 +106,8 @@ export class HaConfigLabels extends LitElement { style=" background-color: ${computeCssColor(label.color)}; border-radius: 10px; - outline: 1px solid var(--outline-color); + border: 1px solid var(--outline-color); + box-sizing: border-box; width: 20px; height: 20px;" >` From b918862bb1de0a8dfc90eac22adb3640a0f2fe3a Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:08:54 +0100 Subject: [PATCH 03/19] Confirm uses to if for clarity (#22816) --- src/translations/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/translations/en.json b/src/translations/en.json index 68eb675f05..19653aba39 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3290,7 +3290,7 @@ "state": "[%key:ui::panel::config::automation::editor::triggers::type::state::label%]", "description": { "picker": "If an entity (or attribute) is in a specific state.", - "no_entity": "Confirm state", + "no_entity": "If state confirmed", "full": "If{hasAttribute, select, \n true { {attribute} of}\n other {}\n} {numberOfEntities, plural,\n =0 {an entity is}\n one {{entities} is}\n other {{entities} are}\n} {numberOfStates, plural,\n =0 {a state}\n other {{states}}\n}{hasDuration, select, \n true { for {duration}} \n other {}\n }" } }, @@ -3353,7 +3353,7 @@ "zone": "[%key:ui::panel::config::automation::editor::triggers::type::zone::label%]", "description": { "picker": "If someone (or something) is in a zone.", - "full": "Confirm {entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} in {zone} {numberOfZones, plural,\n one {zone} \n other {zones}\n} " + "full": "If {entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} in {zone} {numberOfZones, plural,\n one {zone} \n other {zones}\n}" } } } From 17982e0bdc7d843973dff89f8a801f03877ca35d Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:11:20 +0100 Subject: [PATCH 04/19] Fix swapped plural and singular match use in and condition (#22815) * Fix swapped plural and singular match * Test if => if --- src/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/translations/en.json b/src/translations/en.json index 19653aba39..7b7a7c72da 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3242,7 +3242,7 @@ "description": { "picker": "Test if multiple conditions are true.", "no_conditions": "If multiple conditions match", - "full": "If {count} {count, plural,\n one {condition match}\n other {conditions matches}\n}" + "full": "If {count} {count, plural,\n one {condition matches}\n other {conditions match}\n}" } }, "device": { From b83be385141a6d4513afc4848902745bcc31054f Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:28:15 +0100 Subject: [PATCH 05/19] Introduce calendar trigger description (#22814) --- src/common/datetime/format_duration.ts | 60 ++++++++++++++++++++++++++ src/data/automation_i18n.ts | 37 +++++++++++++++- src/translations/en.json | 3 +- 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/common/datetime/format_duration.ts b/src/common/datetime/format_duration.ts index c530a20d4a..9279a8b0ce 100644 --- a/src/common/datetime/format_duration.ts +++ b/src/common/datetime/format_duration.ts @@ -1,5 +1,6 @@ import type { HaDurationData } from "../../components/ha-duration-input"; import type { FrontendLocaleData } from "../../data/translation"; +import { formatListWithAnds } from "../string/format-list"; const leftPad = (num: number) => (num < 10 ? `0${num}` : num); @@ -42,3 +43,62 @@ export const formatDuration = ( } return null; }; + +export const formatDurationLong = ( + locale: FrontendLocaleData, + duration: HaDurationData +) => { + const d = duration.days || 0; + const h = duration.hours || 0; + const m = duration.minutes || 0; + const s = duration.seconds || 0; + const ms = duration.milliseconds || 0; + + const parts: string[] = []; + if (d > 0) { + parts.push( + Intl.NumberFormat(locale.language, { + style: "unit", + unit: "day", + unitDisplay: "long", + }).format(d) + ); + } + if (h > 0) { + parts.push( + Intl.NumberFormat(locale.language, { + style: "unit", + unit: "hour", + unitDisplay: "long", + }).format(h) + ); + } + if (m > 0) { + parts.push( + Intl.NumberFormat(locale.language, { + style: "unit", + unit: "minute", + unitDisplay: "long", + }).format(m) + ); + } + if (s > 0) { + parts.push( + Intl.NumberFormat(locale.language, { + style: "unit", + unit: "second", + unitDisplay: "long", + }).format(s) + ); + } + if (ms > 0) { + parts.push( + Intl.NumberFormat(locale.language, { + style: "unit", + unit: "millisecond", + unitDisplay: "long", + }).format(ms) + ); + } + return formatListWithAnds(locale, parts); +}; diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index 5f8b9eb95c..2651f71246 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -1,6 +1,9 @@ import type { HassConfig } from "home-assistant-js-websocket"; import { ensureArray } from "../common/array/ensure-array"; -import { formatDuration } from "../common/datetime/format_duration"; +import { + formatDuration, + formatDurationLong, +} from "../common/datetime/format_duration"; import { formatTime, formatTimeWithSeconds, @@ -720,6 +723,38 @@ const tryDescribeTrigger = ( }`; } + // Calendar Trigger + if (trigger.trigger === "calendar") { + const calendarEntity = hass.states[trigger.entity_id] + ? computeStateName(hass.states[trigger.entity_id]) + : trigger.entity_id; + + let offsetChoice = trigger.offset.startsWith("-") ? "before" : "after"; + let offset: string | string[] = trigger.offset.startsWith("-") + ? trigger.offset.substring(1).split(":") + : trigger.offset.split(":"); + const duration = { + hours: offset.length > 0 ? +offset[0] : 0, + minutes: offset.length > 1 ? +offset[1] : 0, + seconds: offset.length > 2 ? +offset[2] : 0, + }; + offset = formatDurationLong(hass.locale, duration); + if (offset === "") { + offsetChoice = "other"; + } + + return hass.localize( + `${triggerTranslationBaseKey}.calendar.description.full`, + { + eventChoice: trigger.event, + offsetChoice: offsetChoice, + offset: offset, + hasCalendar: trigger.entity_id ? "true" : "false", + calendar: calendarEntity, + } + ); + } + return ( hass.localize( `ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label` diff --git a/src/translations/en.json b/src/translations/en.json index 7b7a7c72da..cda8a005c5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2995,7 +2995,8 @@ "before": "Before", "after": "After", "description": { - "picker": "When a calendar event starts or ends." + "picker": "When a calendar event starts or ends.", + "full": "When{offsetChoice, select, \n before { it's {offset} before}\n after { it's {offset} after}\n other {}\n} a calendar event{eventChoice, select, \n start { starts}\n end { ends}\n other { starts or ends}\n}{hasCalendar, select, \n true { in {calendar}}\n other {}\n}" } }, "device": { From 991cf83ff3c14ca119c38aed4e1fc0f2af1e794e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:00:34 +0100 Subject: [PATCH 06/19] Update dependency @babel/helper-define-polyfill-provider to v0.6.3 (#22829) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index fbc8dcc112..b19ef25920 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ }, "devDependencies": { "@babel/core": "7.26.0", - "@babel/helper-define-polyfill-provider": "0.6.2", + "@babel/helper-define-polyfill-provider": "0.6.3", "@babel/plugin-proposal-decorators": "7.25.9", "@babel/plugin-transform-runtime": "7.25.9", "@babel/preset-env": "7.26.0", diff --git a/yarn.lock b/yarn.lock index f4dcb76f3a..58f89b9187 100644 --- a/yarn.lock +++ b/yarn.lock @@ -144,9 +144,9 @@ __metadata: languageName: node linkType: hard -"@babel/helper-define-polyfill-provider@npm:0.6.2, @babel/helper-define-polyfill-provider@npm:^0.6.2": - version: 0.6.2 - resolution: "@babel/helper-define-polyfill-provider@npm:0.6.2" +"@babel/helper-define-polyfill-provider@npm:0.6.3, @babel/helper-define-polyfill-provider@npm:^0.6.2": + version: 0.6.3 + resolution: "@babel/helper-define-polyfill-provider@npm:0.6.3" dependencies: "@babel/helper-compilation-targets": "npm:^7.22.6" "@babel/helper-plugin-utils": "npm:^7.22.5" @@ -155,7 +155,7 @@ __metadata: resolve: "npm:^1.14.2" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/bb32ec12024d3f16e70641bc125d2534a97edbfdabbc9f69001ec9c4ce46f877c7a224c566aa6c8c510c3b0def2e43dc4433bf6a40896ba5ce0cef4ea5ccbcff + checksum: 10/b79a77ac8fbf1aaf6c7f99191871760508e87d75a374ff3c39c6599a17d9bb82284797cd451769305764e504546caf22ae63367b22d6e45e32d0a8f4a34aab53 languageName: node linkType: hard @@ -8636,7 +8636,7 @@ __metadata: resolution: "home-assistant-frontend@workspace:." dependencies: "@babel/core": "npm:7.26.0" - "@babel/helper-define-polyfill-provider": "npm:0.6.2" + "@babel/helper-define-polyfill-provider": "npm:0.6.3" "@babel/plugin-proposal-decorators": "npm:7.25.9" "@babel/plugin-transform-runtime": "npm:7.25.9" "@babel/preset-env": "npm:7.26.0" From d47966cdf79e1f50c4b1d31ef2df2b7c41356d89 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:13:04 -0800 Subject: [PATCH 07/19] Allow attaching additional data to schedule in UI (#22798) * Allow attaching additional data to schedule in UI * use expandable --- .../forms/dialog-schedule-block-info.ts | 55 ++++++++++++++----- .../forms/show-dialog-schedule-block-info.ts | 1 + src/translations/en.json | 4 +- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/panels/config/helpers/forms/dialog-schedule-block-info.ts b/src/panels/config/helpers/forms/dialog-schedule-block-info.ts index 7063e57ab2..b4815eec10 100644 --- a/src/panels/config/helpers/forms/dialog-schedule-block-info.ts +++ b/src/panels/config/helpers/forms/dialog-schedule-block-info.ts @@ -1,5 +1,6 @@ import type { CSSResultGroup } from "lit"; import { html, LitElement, nothing } from "lit"; +import memoizeOne from "memoize-one"; import { property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { createCloseHeading } from "../../../../components/ha-dialog"; @@ -13,19 +14,6 @@ import type { } from "./show-dialog-schedule-block-info"; import type { SchemaUnion } from "../../../../components/ha-form/types"; -const SCHEMA = [ - { - name: "from", - required: true, - selector: { time: { no_second: true } }, - }, - { - name: "to", - required: true, - selector: { time: { no_second: true } }, - }, -]; - class DialogScheduleBlockInfo extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -35,10 +23,39 @@ class DialogScheduleBlockInfo extends LitElement { @state() private _params?: ScheduleBlockInfoDialogParams; + private _expand = false; + + private _schema = memoizeOne((expand: boolean) => [ + { + name: "from", + required: true, + selector: { time: { no_second: true } }, + }, + { + name: "to", + required: true, + selector: { time: { no_second: true } }, + }, + { + name: "advanced_settings", + type: "expandable" as const, + flatten: true, + expanded: expand, + schema: [ + { + name: "data", + required: false, + selector: { object: {} }, + }, + ], + }, + ]); + public showDialog(params: ScheduleBlockInfoDialogParams): void { this._params = params; this._error = undefined; this._data = params.block; + this._expand = !!params.block?.data; } public closeDialog(): void { @@ -66,7 +83,7 @@ class DialogScheduleBlockInfo extends LitElement {
) => { + private _computeLabelCallback = ( + schema: SchemaUnion> + ) => { switch (schema.name) { case "from": return this.hass!.localize("ui.dialogs.helper_settings.schedule.start"); case "to": return this.hass!.localize("ui.dialogs.helper_settings.schedule.end"); + case "data": + return this.hass!.localize("ui.dialogs.helper_settings.schedule.data"); + case "advanced_settings": + return this.hass!.localize( + "ui.dialogs.helper_settings.schedule.advanced_settings" + ); } return ""; }; diff --git a/src/panels/config/helpers/forms/show-dialog-schedule-block-info.ts b/src/panels/config/helpers/forms/show-dialog-schedule-block-info.ts index 14a5909592..0a95789f86 100644 --- a/src/panels/config/helpers/forms/show-dialog-schedule-block-info.ts +++ b/src/panels/config/helpers/forms/show-dialog-schedule-block-info.ts @@ -3,6 +3,7 @@ import { fireEvent } from "../../../../common/dom/fire_event"; export interface ScheduleBlockInfo { from: string; to: string; + data?: Record; } export interface ScheduleBlockInfoDialogParams { diff --git a/src/translations/en.json b/src/translations/en.json index cda8a005c5..1b056b3d3b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1612,7 +1612,9 @@ "confirm_delete": "Do you want to delete this item?", "edit_schedule_block": "Edit schedule block", "start": "Start", - "end": "End" + "end": "End", + "data": "Additional data", + "advanced_settings": "Advanced settings" }, "template": { "time": "[%key:ui::panel::developer-tools::tabs::templates::time%]", From cae5540c445c327c3006c1a093fcfc80ed9573ce Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 15 Nov 2024 18:40:38 +0200 Subject: [PATCH 08/19] ZWaveJS: Configuration.resetAll is only supported on CC v4+ (#22823) --- .../zwave_js/zwave_js-node-config.ts | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts index 6ae6654b41..30f1cde47b 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts @@ -22,12 +22,14 @@ import "../../../../../components/buttons/ha-progress-button"; import type { HaProgressButton } from "../../../../../components/buttons/ha-progress-button"; import { computeDeviceName } from "../../../../../data/device_registry"; import type { + ZWaveJSNodeCapabilities, ZWaveJSNodeConfigParam, ZWaveJSNodeConfigParams, ZWaveJSSetConfigParamResult, ZwaveJSNodeMetadata, } from "../../../../../data/zwave_js"; import { + fetchZwaveNodeCapabilities, fetchZwaveNodeConfigParameters, fetchZwaveNodeMetadata, invokeZWaveCCApi, @@ -68,6 +70,8 @@ class ZWaveJSNodeConfig extends LitElement { @state() private _config?: ZWaveJSNodeConfigParams; + @state() private _canResetAll = false; + @state() private _results: Record = {}; @state() private _error?: string; @@ -183,17 +187,19 @@ class ZWaveJSNodeConfig extends LitElement {
` )} -
- - ${this.hass.localize( - "ui.panel.config.zwave_js.node_config.reset_to_default.button_label" - )} - -
+ ${this._canResetAll + ? html`
+ + ${this.hass.localize( + "ui.panel.config.zwave_js.node_config.reset_to_default.button_label" + )} + +
` + : nothing}

${this.hass.localize( "ui.panel.config.zwave_js.node_config.custom_config" @@ -468,10 +474,19 @@ class ZWaveJSNodeConfig extends LitElement { return; } - [this._nodeMetadata, this._config] = await Promise.all([ + let capabilities: ZWaveJSNodeCapabilities | undefined; + [this._nodeMetadata, this._config, capabilities] = await Promise.all([ fetchZwaveNodeMetadata(this.hass, device.id), fetchZwaveNodeConfigParameters(this.hass, device.id), + fetchZwaveNodeCapabilities(this.hass, device.id), ]); + this._canResetAll = + capabilities && + Object.values(capabilities).some((endpoint) => + endpoint.some( + (capability) => capability.id === 0x70 && capability.version >= 4 + ) + ); } private async _openResetDialog(event: Event) { From b8a13dd6ebdb50aad3fe820d9ace41152ec511ca Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 15 Nov 2024 18:55:02 +0200 Subject: [PATCH 09/19] ZWaveJS: Add names to colors in Installer Settings (#22819) --- ...zwave_js-capability-control-color-switch.ts | 18 ++++++++++++++++-- src/translations/en.json | 13 ++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/panels/config/integrations/integration-panels/zwave_js/capability-controls/zwave_js-capability-control-color-switch.ts b/src/panels/config/integrations/integration-panels/zwave_js/capability-controls/zwave_js-capability-control-color-switch.ts index d7facea8ac..69ee23b3f1 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/capability-controls/zwave_js-capability-control-color-switch.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/capability-controls/zwave_js-capability-control-color-switch.ts @@ -8,6 +8,18 @@ import "../../../../../../components/ha-circular-progress"; import { extractApiErrorMessage } from "../../../../../../data/hassio/common"; import "./zwave_js-capability-control-multilevel-switch"; +enum ColorComponent { + "Warm White" = 0, + "Cold White", + Red, + Green, + Blue, + Amber, + Cyan, + Purple, + Index, +} + @customElement("zwave_js-capability-control-color_switch") class ZWaveJSCapabilityColorSwitch extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -20,7 +32,7 @@ class ZWaveJSCapabilityColorSwitch extends LitElement { @property({ type: Number }) public version!: number; - @state() private _color_components?: number[]; + @state() private _color_components?: ColorComponent[]; @state() private _error?: string; @@ -37,7 +49,9 @@ class ZWaveJSCapabilityColorSwitch extends LitElement { ${this.hass.localize( "ui.panel.config.zwave_js.node_installer.capability_controls.color_switch.color_component" )}: - ${color} + ${this.hass.localize( + `ui.panel.config.zwave_js.node_installer.capability_controls.color_switch.colors.${color}` + )}

Date: Sat, 16 Nov 2024 09:02:45 +0100 Subject: [PATCH 10/19] Update dependency @codemirror/autocomplete to v6.18.3 (#22842) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b19ef25920..723026b4a2 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "dependencies": { "@babel/runtime": "7.26.0", "@braintree/sanitize-url": "7.1.0", - "@codemirror/autocomplete": "6.18.2", + "@codemirror/autocomplete": "6.18.3", "@codemirror/commands": "6.7.1", "@codemirror/language": "6.10.3", "@codemirror/legacy-modes": "6.4.2", diff --git a/yarn.lock b/yarn.lock index 58f89b9187..2c199ae782 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1265,9 +1265,9 @@ __metadata: languageName: node linkType: hard -"@codemirror/autocomplete@npm:6.18.2": - version: 6.18.2 - resolution: "@codemirror/autocomplete@npm:6.18.2" +"@codemirror/autocomplete@npm:6.18.3": + version: 6.18.3 + resolution: "@codemirror/autocomplete@npm:6.18.3" dependencies: "@codemirror/language": "npm:^6.0.0" "@codemirror/state": "npm:^6.0.0" @@ -1278,7 +1278,7 @@ __metadata: "@codemirror/state": ^6.0.0 "@codemirror/view": ^6.0.0 "@lezer/common": ^1.0.0 - checksum: 10/35bd17afb53e8c99b1342964616f0bcc13f5f06a5d4e2d9936afdaea61742b1c20b3856d513c5d5676e3a9b6fd95e997c842467d21dfa106845e65ab1720b2f4 + checksum: 10/a9a684cfc4a5d5d7293993a2480c9f3a5c270b2b4a2f192d9179b4458bb30f9f299912915a1d919320c1281c8e15507cfbc76e792f42f304e5f333e4019f5723 languageName: node linkType: hard @@ -8644,7 +8644,7 @@ __metadata: "@babel/runtime": "npm:7.26.0" "@braintree/sanitize-url": "npm:7.1.0" "@bundle-stats/plugin-webpack-filter": "npm:4.16.0" - "@codemirror/autocomplete": "npm:6.18.2" + "@codemirror/autocomplete": "npm:6.18.3" "@codemirror/commands": "npm:6.7.1" "@codemirror/language": "npm:6.10.3" "@codemirror/legacy-modes": "npm:6.4.2" From 0db2b45cc37438bff9862e5a0bb56e2813d7b9a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Nov 2024 10:16:36 +0100 Subject: [PATCH 11/19] Bump cross-spawn from 7.0.3 to 7.0.5 (#22843) Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.5. - [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md) - [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5) --- updated-dependencies: - dependency-name: cross-spawn dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2c199ae782..6320d40038 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6506,13 +6506,13 @@ __metadata: linkType: hard "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": - version: 7.0.3 - resolution: "cross-spawn@npm:7.0.3" + version: 7.0.5 + resolution: "cross-spawn@npm:7.0.5" dependencies: path-key: "npm:^3.1.0" shebang-command: "npm:^2.0.0" which: "npm:^2.0.1" - checksum: 10/e1a13869d2f57d974de0d9ef7acbf69dc6937db20b918525a01dacb5032129bd552d290d886d981e99f1b624cb03657084cc87bd40f115c07ecf376821c729ce + checksum: 10/c95062469d4bdbc1f099454d01c0e77177a3733012d41bf907a71eb8d22d2add43b5adf6a0a14ef4e7feaf804082714d6c262ef4557a1c480b86786c120d18e2 languageName: node linkType: hard From b056b7155789c71125e6b82aa83a1b8a1ed9d641 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 16 Nov 2024 13:20:06 +0100 Subject: [PATCH 12/19] Only download verified translations (#22844) --- build-scripts/gulp/download-translations.js | 1 + 1 file changed, 1 insertion(+) diff --git a/build-scripts/gulp/download-translations.js b/build-scripts/gulp/download-translations.js index b9f2c97583..b12eb228f0 100644 --- a/build-scripts/gulp/download-translations.js +++ b/build-scripts/gulp/download-translations.js @@ -127,6 +127,7 @@ gulp.task("fetch-lokalise", async function () { replace_breaks: false, json_unescaped_slashes: true, export_empty_as: "skip", + filter_data: ["verified"], }) .then((download) => fetch(download.bundle_url)) .then((response) => { From c7dae49c4204f8267a6983b343242017c92e6a1a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 16 Nov 2024 18:23:45 +0100 Subject: [PATCH 13/19] Update dependency marked to v15 (#22782) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 723026b4a2..6a0fb23b52 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch", "lit": "2.8.0", "luxon": "3.5.0", - "marked": "14.1.4", + "marked": "15.0.0", "memoize-one": "6.0.0", "node-vibrant": "3.2.1-alpha.1", "proxy-polyfill": "0.3.2", diff --git a/yarn.lock b/yarn.lock index 6320d40038..929d3a379e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8798,7 +8798,7 @@ __metadata: luxon: "npm:3.5.0" magic-string: "npm:0.30.12" map-stream: "npm:0.0.7" - marked: "npm:14.1.4" + marked: "npm:15.0.0" memoize-one: "npm:6.0.0" mocha: "npm:10.8.2" node-vibrant: "npm:3.2.1-alpha.1" @@ -10522,12 +10522,12 @@ __metadata: languageName: node linkType: hard -"marked@npm:14.1.4": - version: 14.1.4 - resolution: "marked@npm:14.1.4" +"marked@npm:15.0.0": + version: 15.0.0 + resolution: "marked@npm:15.0.0" bin: marked: bin/marked.js - checksum: 10/e3526e7907aa1c13481d205b667a178bd372c01318439e4cd8a3d4b55e3983bccef8c17489129c6a0e31dbecb0b417deff6c27f9f16083faa4eea16a22784a86 + checksum: 10/f7c21c3a3364a52069aac072c7492ebac6eda03f495905d15f1999f246dadbd26d621ff4a4e834a44fa5d26830e576a031b2c00ec104002d38e4e59150a6d6ff languageName: node linkType: hard From f51bc402032081c3c5417a36b4c0a5db3dc5ea2b Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Sun, 17 Nov 2024 07:09:03 -0800 Subject: [PATCH 14/19] Catch yaml errors in script editor (#22853) --- src/panels/config/script/ha-script-editor.ts | 29 ++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index a3eaaea6a4..88da99c7af 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -79,6 +79,8 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { @state() private _errors?: string; + @state() private _yamlErrors?: string; + @state() private _entityId?: string; @state() private _mode: "gui" | "yaml" = "gui"; @@ -602,12 +604,14 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { 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; - this._dirty = true; } private async confirmUnsavedChanged(): Promise { @@ -723,7 +727,21 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { history.back(); } - private _switchUiMode() { + private async _switchUiMode() { + if (this._yamlErrors) { + const result = await showConfirmationDialog(this, { + text: html`${this.hass.localize( + "ui.panel.config.automation.editor.switch_ui_yaml_error" + )}

${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 = "gui"; } @@ -763,6 +781,13 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } private async _saveScript(): Promise { + if (this._yamlErrors) { + showToast(this, { + message: this._yamlErrors, + }); + return; + } + if (!this.scriptId) { const saved = await this._promptScriptAlias(); if (!saved) { From fa821b1c4f8306e7d84133a3e98c2f0adcdd921e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 17 Nov 2024 17:47:56 +0100 Subject: [PATCH 15/19] Update dependency @types/chromecast-caf-receiver to v6.0.18 (#22849) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 6a0fb23b52..bfc7041414 100644 --- a/package.json +++ b/package.json @@ -166,7 +166,7 @@ "@octokit/rest": "21.0.2", "@open-wc/dev-server-hmr": "0.1.4", "@types/babel__plugin-transform-runtime": "7.9.5", - "@types/chromecast-caf-receiver": "6.0.17", + "@types/chromecast-caf-receiver": "6.0.18", "@types/chromecast-caf-sender": "1.0.10", "@types/color-name": "2.0.0", "@types/glob": "8.1.0", diff --git a/yarn.lock b/yarn.lock index 929d3a379e..2e004d88b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3792,10 +3792,10 @@ __metadata: languageName: node linkType: hard -"@types/chromecast-caf-receiver@npm:6.0.17": - version: 6.0.17 - resolution: "@types/chromecast-caf-receiver@npm:6.0.17" - checksum: 10/a8cd00861da900dd56795fc4a7d483c25dfccda9732bea0aaf314e28be2a254cd8c892ebddc44c9ce62087570a9d225767fa3904767993af3e5c79adb80ea966 +"@types/chromecast-caf-receiver@npm:6.0.18": + version: 6.0.18 + resolution: "@types/chromecast-caf-receiver@npm:6.0.18" + checksum: 10/abf21a7d1959a3b78be55ed927fe0764ce70a5c574f3fcb364008eb248f0c19f6c5df714c47640075d51dfe2467f0e281bedf562721e4838a20c50ca638fd306 languageName: node linkType: hard @@ -8713,7 +8713,7 @@ __metadata: "@replit/codemirror-indentation-markers": "npm:6.5.3" "@thomasloven/round-slider": "npm:0.6.0" "@types/babel__plugin-transform-runtime": "npm:7.9.5" - "@types/chromecast-caf-receiver": "npm:6.0.17" + "@types/chromecast-caf-receiver": "npm:6.0.18" "@types/chromecast-caf-sender": "npm:1.0.10" "@types/color-name": "npm:2.0.0" "@types/glob": "npm:8.1.0" From 1f5f6c5f8ab9990be1a7787ef06f23b5b07639f8 Mon Sep 17 00:00:00 2001 From: Matt Way Date: Mon, 18 Nov 2024 03:55:43 +1100 Subject: [PATCH 16/19] Fix back gesture on Android activating buttons (#22852) Touchcancel event cancels touch regardless of cancelled flag Co-authored-by: Benjamin Paul --- .../lovelace/common/directives/action-handler-directive.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/common/directives/action-handler-directive.ts b/src/panels/lovelace/common/directives/action-handler-directive.ts index d39db84b6b..ef931d895b 100644 --- a/src/panels/lovelace/common/directives/action-handler-directive.ts +++ b/src/panels/lovelace/common/directives/action-handler-directive.ts @@ -149,7 +149,10 @@ class ActionHandler extends HTMLElement implements ActionHandlerType { element.actionHandler.end = (ev: Event) => { // Don't respond when moved or scrolled while touch - if (["touchend", "touchcancel"].includes(ev.type) && this.cancelled) { + if ( + ev.type === "touchcancel" || + (ev.type === "touchend" && this.cancelled) + ) { return; } const target = ev.target as HTMLElement; From 03ea08f98c7202989897a69a2b42dc197f1da140 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 18 Nov 2024 07:53:01 +0100 Subject: [PATCH 17/19] Create a section when dropping card on the create section button (#22790) * Create a section when dropping card on the create section button * Add translations * Add title by default * And case when moving a heading card --- .../lovelace/views/hui-sections-view.ts | 170 +++++++++++++++--- src/translations/en.json | 1 + 2 files changed, 144 insertions(+), 27 deletions(-) diff --git a/src/panels/lovelace/views/hui-sections-view.ts b/src/panels/lovelace/views/hui-sections-view.ts index 8dff28e127..944e3a1632 100644 --- a/src/panels/lovelace/views/hui-sections-view.ts +++ b/src/panels/lovelace/views/hui-sections-view.ts @@ -7,7 +7,7 @@ import { mdiViewGridPlus, } from "@mdi/js"; import type { CSSResultGroup, PropertyValues } from "lit"; -import { LitElement, css, html, nothing } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { repeat } from "lit/directives/repeat"; @@ -20,6 +20,7 @@ import "../../../components/ha-sortable"; import "../../../components/ha-svg-icon"; import type { LovelaceViewElement } from "../../../data/lovelace"; import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; +import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../types"; @@ -27,8 +28,19 @@ import type { HuiBadge } from "../badges/hui-badge"; import "../badges/hui-view-badges"; import type { HuiCard } from "../cards/hui-card"; import "../components/hui-badge-edit-mode"; -import { addSection, deleteSection, moveSection } from "../editor/config-util"; -import { findLovelaceContainer } from "../editor/lovelace-path"; +import { + addSection, + deleteSection, + moveCard, + moveSection, +} from "../editor/config-util"; +import type { LovelaceCardPath } from "../editor/lovelace-path"; +import { + findLovelaceContainer, + findLovelaceItems, + getLovelaceContainerPath, + parseLovelaceCardPath, +} from "../editor/lovelace-path"; import { showEditSectionDialog } from "../editor/section-editor/show-edit-section-dialog"; import type { HuiSection } from "../sections/hui-section"; import type { Lovelace } from "../types"; @@ -231,19 +243,35 @@ export class SectionsView extends LitElement implements LovelaceViewElement { )} ${editMode ? html` - +
+ + +
+ ` : nothing} ${editMode && this._config?.cards?.length @@ -280,6 +308,52 @@ export class SectionsView extends LitElement implements LovelaceViewElement { `; } + private _defaultSection(includeHeading: boolean): LovelaceSectionConfig { + return { + type: "grid", + cards: includeHeading + ? [ + { + type: "heading", + heading: this.hass!.localize( + "ui.panel.lovelace.editor.section.default_section_title" + ), + }, + ] + : [], + }; + } + + private _handleCardAdded(ev) { + const { data } = ev.detail; + const oldPath = data as LovelaceCardPath; + + const { cardIndex } = parseLovelaceCardPath(oldPath); + const containerPath = getLovelaceContainerPath(oldPath); + const cards = findLovelaceItems( + "cards", + this.lovelace!.config, + containerPath + ); + const cardConfig = cards![cardIndex]; + + const configWithNewSection = addSection( + this.lovelace!.config, + this.index!, + this._defaultSection(cardConfig.type !== "heading") // If we move a heading card, we don't want to include a heading in the new section + ); + const viewConfig = configWithNewSection.views[ + this.index! + ] as LovelaceViewConfig; + const newPath = [ + this.index!, + viewConfig.sections!.length - 1, + 1, + ] as LovelaceCardPath; + const newConfig = moveCard(configWithNewSection, oldPath, newPath); + this.lovelace!.saveConfig(newConfig); + } + private _importedCardSectionConfig = memoizeOne( (cards: LovelaceCardConfig[]) => ({ type: "grid", @@ -288,17 +362,11 @@ export class SectionsView extends LitElement implements LovelaceViewElement { ); private _createSection(): void { - const newConfig = addSection(this.lovelace!.config, this.index!, { - type: "grid", - cards: [ - { - type: "heading", - heading: this.hass!.localize( - "ui.panel.lovelace.editor.section.default_section_title" - ), - }, - ], - }); + const newConfig = addSection( + this.lovelace!.config, + this.index!, + this._defaultSection(true) + ); this.lovelace!.saveConfig(newConfig); } @@ -415,8 +483,55 @@ export class SectionsView extends LitElement implements LovelaceViewElement { padding: 8px; } - .create-section { + .create-section-container { + position: relative; + display: flex; + flex-direction: column; margin-top: 36px; + } + + .create-section-container .card { + display: none; + } + + .create-section-container:has(.card) .drop-helper { + display: flex; + } + .create-section-container:has(.card) .create-section { + display: none; + } + + .drop-helper { + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + outline: none; + background: none; + cursor: pointer; + border-radius: var(--ha-card-border-radius, 12px); + border: 2px dashed var(--primary-color); + height: calc(var(--row-height) + 2 * (var(--row-gap) + 2px)); + padding: 8px; + box-sizing: border-box; + width: 100%; + --ha-ripple-color: var(--primary-color); + --ha-ripple-hover-opacity: 0.04; + --ha-ripple-pressed-opacity: 0.12; + } + + .drop-helper p { + color: var(--primary-text-color); + font-size: 16px; + font-weight: 400; + line-height: 24px; + text-align: center; + } + + .create-section { + display: block; + position: relative; outline: none; background: none; cursor: pointer; @@ -426,6 +541,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement { height: calc(var(--row-height) + 2 * (var(--row-gap) + 2px)); padding: 8px; box-sizing: border-box; + width: 100%; --ha-ripple-color: var(--primary-color); --ha-ripple-hover-opacity: 0.04; --ha-ripple-pressed-opacity: 0.12; diff --git a/src/translations/en.json b/src/translations/en.json index df961152e8..8d775b4ce5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5895,6 +5895,7 @@ "section": { "add_badge": "Add badge", "add_card": "[%key:ui::panel::lovelace::editor::edit_card::add%]", + "drop_card_create_section": "Drop card here to create a new section", "create_section": "Create section", "default_section_title": "New section", "imported_cards_title": "Imported cards", From 1990b8fa842c3c9eab824fd739a81036b9a46005 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 08:54:58 +0100 Subject: [PATCH 18/19] Bump softprops/action-gh-release from 2.0.9 to 2.1.0 (#22854) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4f2e82c56b..1ff067e3c8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -55,7 +55,7 @@ jobs: script/release - name: Upload release assets - uses: softprops/action-gh-release@v2.0.9 + uses: softprops/action-gh-release@v2.1.0 with: files: | dist/*.whl From 23b55484c35976f02d91aed07986613007521714 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 18 Nov 2024 09:43:52 +0100 Subject: [PATCH 19/19] Improve grid size editor (#22697) * Use table instead of grid * Add animation * Change size * Simplify precideMode logic * Add tooltip and improve slider style * Improve size * Back to default instead of min * Limit number of cells for the grid when more than 24 cells --- src/components/ha-grid-size-picker.ts | 122 ++++++------ .../lovelace/common/compute-card-grid-size.ts | 10 +- .../card-editor/ha-grid-layout-slider.ts | 182 +++++++++++++++++- .../card-editor/hui-card-layout-editor.ts | 75 +++----- 4 files changed, 273 insertions(+), 116 deletions(-) diff --git a/src/components/ha-grid-size-picker.ts b/src/components/ha-grid-size-picker.ts index f3b5607c21..49a293e33d 100644 --- a/src/components/ha-grid-size-picker.ts +++ b/src/components/ha-grid-size-picker.ts @@ -2,9 +2,7 @@ import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider"; import "./ha-icon-button"; - import { mdiRestore } from "@mdi/js"; -import { classMap } from "lit/directives/class-map"; import { styleMap } from "lit/directives/style-map"; import { fireEvent } from "../common/dom/fire_event"; import { conditionalClamp } from "../common/number/clamp"; @@ -20,7 +18,7 @@ export class HaGridSizeEditor extends LitElement { @property({ attribute: false }) public rows = 8; - @property({ attribute: false }) public columns = 4; + @property({ attribute: false }) public columns = 12; @property({ attribute: false }) public rowMin?: number; @@ -32,6 +30,8 @@ export class HaGridSizeEditor extends LitElement { @property({ attribute: false }) public isDefault?: boolean; + @property({ attribute: false }) public step: number = 1; + @state() public _localValue?: CardGridSize = { rows: 1, columns: 1 }; protected willUpdate(changedProperties) { @@ -51,8 +51,9 @@ export class HaGridSizeEditor extends LitElement { const rowMin = this.rowMin ?? 1; const rowMax = this.rowMax ?? this.rows; - const columnMin = this.columnMin ?? 1; - const columnMax = this.columnMax ?? this.columns; + const columnMin = Math.ceil((this.columnMin ?? 1) / this.step) * this.step; + const columnMax = + Math.ceil((this.columnMax ?? this.columns) / this.step) * this.step; const rowValue = autoHeight ? rowMin : this._localValue?.rows; const columnValue = this._localValue?.columns; @@ -67,9 +68,11 @@ export class HaGridSizeEditor extends LitElement { .max=${columnMax} .range=${this.columns} .value=${fullWidth ? this.columns : this.value?.columns} + .step=${this.step} @value-changed=${this._valueChanged} @slider-moved=${this._sliderMoved} .disabled=${disabledColumns} + tooltip-mode="always" > ${!this.isDefault ? html` @@ -102,34 +106,44 @@ export class HaGridSizeEditor extends LitElement { ` : nothing} -
-
- ${Array(this.rows * this.columns) +
+ + ${Array(this.rows) .fill(0) .map((_, index) => { - const row = Math.floor(index / this.columns) + 1; - const column = (index % this.columns) + 1; + const row = index + 1; return html` -
+ + ${Array(this.columns) + .fill(0) + .map((__, columnIndex) => { + const column = columnIndex + 1; + if ( + column % this.step !== 0 || + (this.columns > 24 && column % 3 !== 0) + ) { + return nothing; + } + return html` + + `; + })} + `; })} - -
-
-
+
+
`; @@ -223,42 +237,40 @@ export class HaGridSizeEditor extends LitElement { } .reset { grid-area: reset; + --mdc-icon-button-size: 36px; } .preview { position: relative; grid-area: preview; } - .preview > div { - position: relative; - display: grid; - grid-template-columns: repeat(var(--total-columns), 1fr); - grid-template-rows: repeat(var(--total-rows), 25px); - gap: 4px; + .preview table, + .preview tr, + .preview td { + border: 2px dotted var(--divider-color); + border-collapse: collapse; } - .preview .cell { - background-color: var(--disabled-color); - grid-column: span 1; - grid-row: span 1; - border-radius: 4px; - opacity: 0.2; - cursor: pointer; - } - .preview .selected { - position: absolute; - pointer-events: none; - top: 0; - left: 0; - height: 100%; + .preview table { width: 100%; } - .selected .cell { - background-color: var(--primary-color); - grid-column: 1 / span min(var(--columns, 0), var(--total-columns)); - grid-row: 1 / span min(var(--rows, 0), var(--total-rows)); - opacity: 0.5; + .preview tr { + height: 30px; } - .preview.full-width .selected .cell { - grid-column: 1 / -1; + .preview td { + cursor: pointer; + } + .preview-card { + position: absolute; + top: 0; + left: 0; + background-color: var(--primary-color); + opacity: 0.3; + border-radius: 8px; + height: calc(var(--rows, 1) * 30px); + width: calc(var(--columns, 1) * 100% / var(--total-columns, 12)); + pointer-events: none; + transition: + width ease-in-out 180ms, + height ease-in-out 180ms; } `, ]; diff --git a/src/panels/lovelace/common/compute-card-grid-size.ts b/src/panels/lovelace/common/compute-card-grid-size.ts index cc0be5ac76..49bc4946ca 100644 --- a/src/panels/lovelace/common/compute-card-grid-size.ts +++ b/src/panels/lovelace/common/compute-card-grid-size.ts @@ -3,16 +3,11 @@ import type { LovelaceGridOptions, LovelaceLayoutOptions } from "../types"; export const GRID_COLUMN_MULTIPLIER = 3; -export const multiplyBy = ( +const multiplyBy = ( value: T, multiplier: number ): T => (typeof value === "number" ? ((value * multiplier) as T) : value); -export const divideBy = ( - value: T, - divider: number -): T => (typeof value === "number" ? (Math.ceil(value / divider) as T) : value); - export const migrateLayoutToGridOptions = ( options: LovelaceLayoutOptions ): LovelaceGridOptions => { @@ -42,6 +37,9 @@ export type CardGridSize = { columns: number | "full"; }; +export const isPreciseMode = (options: LovelaceGridOptions) => + typeof options.columns === "number" && options.columns % 3 !== 0; + export const computeCardGridSize = ( options: LovelaceGridOptions ): CardGridSize => { diff --git a/src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts b/src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts index 206436ce95..e133be40c1 100644 --- a/src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts +++ b/src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts @@ -23,6 +23,8 @@ const A11Y_KEY_CODES = new Set([ "End", ]); +type TooltipMode = "never" | "always" | "interaction"; + @customElement("ha-grid-layout-slider") export class HaGridLayoutSlider extends LitElement { @property({ type: Boolean, reflect: true }) @@ -34,6 +36,9 @@ export class HaGridLayoutSlider extends LitElement { @property({ attribute: "touch-action" }) public touchAction?: string; + @property({ attribute: "tooltip-mode" }) + public tooltipMode: TooltipMode = "interaction"; + @property({ type: Number }) public value?: number; @@ -52,6 +57,9 @@ export class HaGridLayoutSlider extends LitElement { @state() public pressed = false; + @state() + public tooltipVisible = false; + private _mc?: HammerManager; private get _range() { @@ -135,11 +143,13 @@ export class HaGridLayoutSlider extends LitElement { this._mc.on("panstart", () => { if (this.disabled) return; this.pressed = true; + this._showTooltip(); savedValue = this.value; }); this._mc.on("pancancel", () => { if (this.disabled) return; this.pressed = false; + this._hideTooltip(); this.value = savedValue; }); this._mc.on("panmove", (e) => { @@ -152,6 +162,7 @@ export class HaGridLayoutSlider extends LitElement { this._mc.on("panend", (e) => { if (this.disabled) return; this.pressed = false; + this._hideTooltip(); const percentage = this._getPercentageFromEvent(e); const value = this._percentageToValue(percentage); this.value = this._steppedValue(this._boundedValue(value)); @@ -223,6 +234,23 @@ export class HaGridLayoutSlider extends LitElement { fireEvent(this, "value-changed", { value: this.value }); } + private _tooltipTimeout?: number; + + _showTooltip() { + if (this._tooltipTimeout != null) window.clearTimeout(this._tooltipTimeout); + this.tooltipVisible = true; + } + + _hideTooltip(delay?: number) { + if (!delay) { + this.tooltipVisible = false; + return; + } + this._tooltipTimeout = window.setTimeout(() => { + this.tooltipVisible = false; + }, delay); + } + private _getPercentageFromEvent = (e: HammerInput) => { if (this.vertical) { const y = e.center.y; @@ -236,6 +264,30 @@ export class HaGridLayoutSlider extends LitElement { return Math.max(Math.min(1, (x - offset) / total), 0); }; + private _renderTooltip() { + if (this.tooltipMode === "never") return nothing; + + const position = this.vertical ? "left" : "top"; + + const visible = + this.tooltipMode === "always" || + (this.tooltipVisible && this.tooltipMode === "interaction"); + + const value = this._boundedValue(this._steppedValue(this.value ?? 0)); + + return html` + + `; + } + protected render(): TemplateResult { return html`
+ + ${Array(this._range / this.step) + .fill(0) + .map((_, i) => { + const percentage = i / (this._range / this.step); + const disabled = + this.min >= i * this.step || i * this.step > this.max; + if (disabled) { + return nothing; + } + return html` +
+ `; + })} ${this.value !== undefined ? html`
` : nothing} + ${this._renderTooltip()} `; @@ -269,7 +341,7 @@ export class HaGridLayoutSlider extends LitElement { return css` :host { display: block; - --grid-layout-slider: 48px; + --grid-layout-slider: 36px; height: var(--grid-layout-slider); width: 100%; outline: none; @@ -297,6 +369,7 @@ export class HaGridLayoutSlider extends LitElement { } .slider * { pointer-events: none; + user-select: none; } .track { position: absolute; @@ -315,12 +388,11 @@ export class HaGridLayoutSlider extends LitElement { position: absolute; inset: 0; background: var(--disabled-color); - opacity: 0.2; + opacity: 0.4; } .active { position: absolute; - background: grey; - opacity: 0.7; + background: var(--primary-color); top: 0; right: calc(var(--max) * 100%); bottom: 0; @@ -351,6 +423,27 @@ export class HaGridLayoutSlider extends LitElement { height: 16px; width: 100%; } + .dot { + position: absolute; + top: 0; + bottom: 0; + opacity: 0.6; + margin: auto; + width: 4px; + height: 4px; + flex-shrink: 0; + transform: translate(-50%, 0); + background: var(--card-background-color); + left: calc(var(--value, 0%) * 100%); + border-radius: 2px; + } + :host([vertical]) .dot { + transform: translate(0, -50%); + left: 0; + right: 0; + bottom: inherit; + top: calc(var(--value, 0%) * 100%); + } .handle::after { position: absolute; inset: 0; @@ -358,7 +451,7 @@ export class HaGridLayoutSlider extends LitElement { border-radius: 2px; height: 100%; margin: auto; - background: grey; + background: var(--primary-color); content: ""; } :host([vertical]) .handle::after { @@ -374,9 +467,88 @@ export class HaGridLayoutSlider extends LitElement { :host(:disabled) .active { background: var(--disabled-color); } + + .tooltip { + position: absolute; + background-color: var(--clear-background-color); + color: var(--primary-text-color); + font-size: var(--control-slider-tooltip-font-size); + border-radius: 0.8em; + padding: 0.2em 0.4em; + opacity: 0; + white-space: nowrap; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + transition: + opacity 180ms ease-in-out, + left 180ms ease-in-out, + bottom 180ms ease-in-out; + --handle-spacing: calc(2 * var(--handle-margin) + var(--handle-size)); + --slider-tooltip-margin: 0px; + --slider-tooltip-range: 100%; + --slider-tooltip-offset: 0px; + --slider-tooltip-position: calc( + min( + max( + var(--value) * var(--slider-tooltip-range) + + var(--slider-tooltip-offset), + 0% + ), + 100% + ) + ); + } + .tooltip.start { + --slider-tooltip-offset: calc(-0.5 * (var(--handle-spacing))); + } + .tooltip.end { + --slider-tooltip-offset: calc(0.5 * (var(--handle-spacing))); + } + .tooltip.cursor { + --slider-tooltip-range: calc(100% - var(--handle-spacing)); + --slider-tooltip-offset: calc(0.5 * (var(--handle-spacing))); + } + .tooltip.show-handle { + --slider-tooltip-range: calc(100% - var(--handle-spacing)); + --slider-tooltip-offset: calc(0.5 * (var(--handle-spacing))); + } + .tooltip.visible { + opacity: 1; + } + .tooltip.top { + transform: translate3d(-50%, -100%, 0); + top: var(--slider-tooltip-margin); + left: 50%; + } + .tooltip.bottom { + transform: translate3d(-50%, 100%, 0); + bottom: var(--slider-tooltip-margin); + left: 50%; + } + .tooltip.left { + transform: translate3d(-100%, -50%, 0); + top: 50%; + left: var(--slider-tooltip-margin); + } + .tooltip.right { + transform: translate3d(100%, -50%, 0); + top: 50%; + right: var(--slider-tooltip-margin); + } + :host(:not([vertical])) .tooltip.top, + :host(:not([vertical])) .tooltip.bottom { + left: var(--slider-tooltip-position); + } + :host([vertical]) .tooltip.right, + :host([vertical]) .tooltip.left { + top: var(--slider-tooltip-position); + } + .pressed .handle { transition: none; } + .pressed .tooltip { + transition: opacity 180ms ease-in-out; + } `; } } diff --git a/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts b/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts index 494dcd9edb..a79e6eeccd 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts @@ -26,16 +26,12 @@ import type { HuiCard } from "../../cards/hui-card"; import type { CardGridSize } from "../../common/compute-card-grid-size"; import { computeCardGridSize, - divideBy, GRID_COLUMN_MULTIPLIER, + isPreciseMode, migrateLayoutToGridOptions, - multiplyBy, } from "../../common/compute-card-grid-size"; import type { LovelaceGridOptions } from "../../types"; -const computePreciseMode = (columns?: number | string) => - typeof columns === "number" && columns % 3 !== 0; - @customElement("hui-card-layout-editor") export class HuiCardLayoutEditor extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -63,22 +59,6 @@ export class HuiCardLayoutEditor extends LitElement { private _computeCardGridSize = memoizeOne(computeCardGridSize); - private _simplifyOptions = ( - options: LovelaceGridOptions - ): LovelaceGridOptions => ({ - ...options, - columns: divideBy(options.columns, GRID_COLUMN_MULTIPLIER), - max_columns: divideBy(options.max_columns, GRID_COLUMN_MULTIPLIER), - min_columns: divideBy(options.min_columns, GRID_COLUMN_MULTIPLIER), - }); - - private _standardizeOptions = (options: LovelaceGridOptions) => ({ - ...options, - columns: multiplyBy(options.columns, GRID_COLUMN_MULTIPLIER), - max_columns: multiplyBy(options.max_columns, GRID_COLUMN_MULTIPLIER), - min_columns: multiplyBy(options.min_columns, GRID_COLUMN_MULTIPLIER), - }); - private _isDefault = memoizeOne( (options?: LovelaceGridOptions) => options?.columns === undefined && options?.rows === undefined @@ -101,14 +81,11 @@ export class HuiCardLayoutEditor extends LitElement { this._defaultGridOptions ); - const gridOptions = this._preciseMode - ? options - : this._simplifyOptions(options); + const gridOptions = options; const gridValue = this._computeCardGridSize(gridOptions); const columnSpan = this.sectionConfig.column_span ?? 1; - const gridTotalColumns = - (12 * columnSpan) / (this._preciseMode ? 1 : GRID_COLUMN_MULTIPLIER); + const gridTotalColumns = 12 * columnSpan; return html`
@@ -173,7 +150,7 @@ export class HuiCardLayoutEditor extends LitElement { : html` @@ -267,17 +245,21 @@ export class HuiCardLayoutEditor extends LitElement { protected willUpdate(changedProps: PropertyValues): void { super.willUpdate(changedProps); if (changedProps.has("config")) { - const columns = this.config.grid_options?.columns; - const preciseMode = computePreciseMode(columns); + const options = this.config.grid_options; + + // Reset precise mode when grid options config is reset + if (!options) { + this._preciseMode = this._defaultGridOptions + ? isPreciseMode(this._defaultGridOptions) + : false; + return; + } + // Force precise mode if columns count is not a multiple of 3 + const preciseMode = isPreciseMode(options); if (!this._preciseMode && preciseMode) { this._preciseMode = preciseMode; } - // Reset precise mode when grid options config is reset - if (columns === undefined) { - const defaultColumns = this._defaultGridOptions?.columns; - this._preciseMode = computePreciseMode(defaultColumns); - } } } @@ -296,18 +278,10 @@ export class HuiCardLayoutEditor extends LitElement { ev.stopPropagation(); const value = ev.detail.value as CardGridSize; - const gridOptions = { - columns: value.columns, - rows: value.rows, - }; - - const newOptions = this._preciseMode - ? gridOptions - : this._standardizeOptions(gridOptions); - this._updateGridOptions({ ...this.config.grid_options, - ...newOptions, + columns: value.columns, + rows: value.rows, }); } @@ -322,7 +296,7 @@ export class HuiCardLayoutEditor extends LitElement { const value = ev.target.checked; this._updateGridOptions({ ...this.config.grid_options, - columns: value ? "full" : (this._defaultGridOptions?.min_columns ?? 1), + columns: value ? "full" : undefined, }); } @@ -331,13 +305,14 @@ export class HuiCardLayoutEditor extends LitElement { this._preciseMode = ev.target.checked; if (this._preciseMode) return; - const newOptions = this._standardizeOptions( - this._simplifyOptions(this.config.grid_options ?? {}) - ); - if (newOptions.columns !== this.config.grid_options?.columns) { + const columns = this.config.grid_options?.columns; + + if (typeof columns === "number" && columns % GRID_COLUMN_MULTIPLIER !== 0) { + const newColumns = + Math.ceil(columns / GRID_COLUMN_MULTIPLIER) * GRID_COLUMN_MULTIPLIER; this._updateGridOptions({ ...this.config.grid_options, - columns: newOptions.columns, + columns: newColumns, }); } }