diff --git a/package.json b/package.json index 583d05fc240b..d172d911de03 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "color-name": "2.0.0", "comlink": "4.4.1", "core-js": "3.37.0", - "cropperjs": "1.6.1", + "cropperjs": "1.6.2", "date-fns": "3.6.0", "date-fns-tz": "3.1.3", "deep-clone-simple": "1.1.1", diff --git a/pyproject.toml b/pyproject.toml index b25adda887fd..221beef1ae0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20240424.0" +version = "20240424.1" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/components/ha-filter-devices.ts b/src/components/ha-filter-devices.ts index a88b957ed775..66da797b1e53 100644 --- a/src/components/ha-filter-devices.ts +++ b/src/components/ha-filter-devices.ts @@ -69,7 +69,7 @@ export class HaFilterDevices extends LitElement { @value-changed=${this._handleSearchChange} > - + +
+ ${this.hass.localize( + "ui.panel.config.entities.picker.headers.domain" + )} + ${this.value?.length + ? html`
${this.value?.length}
+ ` + : nothing} +
+ ${this._shouldRender + ? html` + + + ${repeat( + this._domains(this.hass.states, this._filter), + (i) => i, + (domain) => + html` + + ${domainToName(this.hass.localize, domain)} + ` + )} + ` + : nothing} + + `; + } + + private _domains = memoizeOne((states, filter) => { + const domains = new Set(); + Object.keys(states).forEach((entityId) => { + domains.add(computeDomain(entityId)); + }); + return Array.from(domains) + .filter((domain) => !filter || domain.toLowerCase().includes(filter)) + .sort((a, b) => stringCompare(a, b, this.hass.locale.language)); + }); + + protected updated(changed) { + if (changed.has("expanded") && this.expanded) { + setTimeout(() => { + if (!this.expanded) return; + this.renderRoot.querySelector("mwc-list")!.style.height = + `${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input + }, 300); + } + } + + private _expandedWillChange(ev) { + this._shouldRender = ev.detail.expanded; + } + + private _expandedChanged(ev) { + this.expanded = ev.detail.expanded; + } + + private _handleItemClick(ev) { + const listItem = ev.target.closest("ha-check-list-item"); + const value = listItem?.value; + if (!value) { + return; + } + if (this.value?.includes(value)) { + this.value = this.value?.filter((val) => val !== value); + } else { + this.value = [...(this.value || []), value]; + } + + listItem.selected = this.value.includes(value); + + fireEvent(this, "data-table-filter-changed", { + value: this.value, + items: undefined, + }); + } + + private _clearFilter(ev) { + ev.preventDefault(); + this.value = undefined; + fireEvent(this, "data-table-filter-changed", { + value: undefined, + items: undefined, + }); + } + + private _handleSearchChange(ev: CustomEvent) { + this._filter = ev.detail.value.toLowerCase(); + } + + static get styles(): CSSResultGroup { + return [ + haStyleScrollbar, + css` + :host { + border-bottom: 1px solid var(--divider-color); + } + :host([expanded]) { + flex: 1; + height: 0; + } + ha-expansion-panel { + --ha-card-border-radius: 0; + --expansion-panel-content-padding: 0; + } + .header { + display: flex; + align-items: center; + } + .header ha-icon-button { + margin-inline-start: auto; + margin-inline-end: 8px; + } + .badge { + display: inline-block; + margin-left: 8px; + margin-inline-start: 8px; + margin-inline-end: 0; + min-width: 16px; + box-sizing: border-box; + border-radius: 50%; + font-weight: 400; + font-size: 11px; + background-color: var(--primary-color); + line-height: 16px; + text-align: center; + padding: 0px 2px; + color: var(--text-primary-color); + } + search-input-outlined { + display: block; + padding: 0 8px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-filter-domains": HaFilterDomains; + } +} diff --git a/src/components/ha-filter-entities.ts b/src/components/ha-filter-entities.ts index ee92c9d0c369..2f3f1f7b457f 100644 --- a/src/components/ha-filter-entities.ts +++ b/src/components/ha-filter-entities.ts @@ -71,7 +71,7 @@ export class HaFilterEntities extends LitElement { @value-changed=${this._handleSearchChange} > - + - + ${repeat( this._integrations(this._manifests, this._filter, this.value), (i) => i.domain, diff --git a/src/components/map/ha-locations-editor.ts b/src/components/map/ha-locations-editor.ts index e0f1c1d8f308..db80ec76a920 100644 --- a/src/components/map/ha-locations-editor.ts +++ b/src/components/map/ha-locations-editor.ts @@ -19,7 +19,7 @@ import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map"; -import type { HomeAssistant } from "../../types"; +import type { HomeAssistant, ThemeMode } from "../../types"; import "../ha-input-helper-text"; import "./ha-map"; import type { HaMap } from "./ha-map"; @@ -61,7 +61,8 @@ export class HaLocationsEditor extends LitElement { @property({ type: Number }) public zoom = 16; - @property({ type: Boolean }) public darkMode = false; + @property({ attribute: "theme-mode", type: String }) + public themeMode: ThemeMode = "auto"; @state() private _locationMarkers?: Record; @@ -133,7 +134,7 @@ export class HaLocationsEditor extends LitElement { .layers=${this._getLayers(this._circles, this._locationMarkers)} .zoom=${this.zoom} .autoFit=${this.autoFit} - ?darkMode=${this.darkMode} + .themeMode=${this.themeMode} > ${this.helper ? html`${this.helper}` diff --git a/src/components/map/ha-map.ts b/src/components/map/ha-map.ts index 1fcbceace41b..ace050182240 100644 --- a/src/components/map/ha-map.ts +++ b/src/components/map/ha-map.ts @@ -1,32 +1,32 @@ +import { isToday } from "date-fns"; import type { Circle, CircleMarker, - LatLngTuple, LatLngExpression, + LatLngTuple, Layer, Map, Marker, Polyline, } from "leaflet"; -import { isToday } from "date-fns"; -import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit"; +import { CSSResultGroup, PropertyValues, ReactiveElement, css } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { formatDateTime } from "../../common/datetime/format_date_time"; +import { + formatTimeWeekday, + formatTimeWithSeconds, +} from "../../common/datetime/format_time"; import { LeafletModuleType, setupLeafletMap, } from "../../common/dom/setup-leaflet-map"; -import { - formatTimeWithSeconds, - formatTimeWeekday, -} from "../../common/datetime/format_time"; -import { formatDateTime } from "../../common/datetime/format_date_time"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; import { loadPolyfillIfNeeded } from "../../resources/resize-observer.polyfill"; -import { HomeAssistant } from "../../types"; +import { HomeAssistant, ThemeMode } from "../../types"; +import { isTouch } from "../../util/is_touch"; import "../ha-icon-button"; import "./ha-entity-marker"; -import { isTouch } from "../../util/is_touch"; const getEntityId = (entity: string | HaMapEntity): string => typeof entity === "string" ? entity : entity.entity_id; @@ -69,7 +69,8 @@ export class HaMap extends ReactiveElement { @property({ type: Boolean }) public fitZones = false; - @property({ type: Boolean }) public darkMode = false; + @property({ attribute: "theme-mode", type: String }) + public themeMode: ThemeMode = "auto"; @property({ type: Number }) public zoom = 14; @@ -154,7 +155,7 @@ export class HaMap extends ReactiveElement { } if ( - !changedProps.has("darkMode") && + !changedProps.has("themeMode") && (!changedProps.has("hass") || (oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode)) ) { @@ -163,12 +164,18 @@ export class HaMap extends ReactiveElement { this._updateMapStyle(); } + private get _darkMode() { + return ( + this.themeMode === "dark" || + (this.themeMode === "auto" && Boolean(this.hass.themes.darkMode)) + ); + } + private _updateMapStyle(): void { - const darkMode = this.darkMode || (this.hass.themes.darkMode ?? false); - const forcedDark = this.darkMode; const map = this.renderRoot.querySelector("#map"); - map!.classList.toggle("dark", darkMode); - map!.classList.toggle("forced-dark", forcedDark); + map!.classList.toggle("dark", this._darkMode); + map!.classList.toggle("forced-dark", this.themeMode === "dark"); + map!.classList.toggle("forced-light", this.themeMode === "light"); } private async _loadMap(): Promise { @@ -398,8 +405,7 @@ export class HaMap extends ReactiveElement { "--dark-primary-color" ); - const className = - this.darkMode || this.hass.themes.darkMode ? "dark" : "light"; + const className = this._darkMode ? "dark" : "light"; for (const entity of this.entities) { const stateObj = hass.states[getEntityId(entity)]; @@ -543,27 +549,30 @@ export class HaMap extends ReactiveElement { background: #090909; } #map.forced-dark { + color: #ffffff; --map-filter: invert(0.9) hue-rotate(170deg) brightness(1.5) contrast(1.2) saturate(0.3); } + #map.forced-light { + background: #ffffff; + color: #000000; + --map-filter: invert(0); + } #map:active { cursor: grabbing; cursor: -moz-grabbing; cursor: -webkit-grabbing; } - .light { - color: #000000; - } - .dark { - color: #ffffff; - } .leaflet-tile-pane { filter: var(--map-filter); } .dark .leaflet-bar a { - background-color: var(--card-background-color, #1c1c1c); + background-color: #1c1c1c; color: #ffffff; } + .dark .leaflet-bar a:hover { + background-color: #313131; + } .leaflet-marker-draggable { cursor: move !important; } diff --git a/src/onboarding/onboarding-location.ts b/src/onboarding/onboarding-location.ts index 3027a93f9e5a..f4c8d505b703 100644 --- a/src/onboarding/onboarding-location.ts +++ b/src/onboarding/onboarding-location.ts @@ -41,7 +41,7 @@ import type { HomeAssistant } from "../types"; import { onBoardingStyles } from "./styles"; const AMSTERDAM: [number, number] = [52.3731339, 4.8903147]; -const mql = matchMedia("(prefers-color-scheme: dark)"); +const darkMql = matchMedia("(prefers-color-scheme: dark)"); const LOCATION_MARKER_ID = "location"; @customElement("onboarding-location") @@ -199,7 +199,7 @@ class OnboardingLocation extends LitElement { this._highlightedMarker )} zoom="14" - .darkMode=${mql.matches} + .themeMode=${darkMql.matches ? "dark" : "light"} .disabled=${this._working} @location-updated=${this._locationChanged} @marker-clicked=${this._markerClicked} diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index d11b038beb0b..c7c8837ae1ce 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -15,6 +15,7 @@ import { mdiPlus, mdiRobotHappy, mdiTag, + mdiTextureBox, mdiToggleSwitch, mdiToggleSwitchOffOutline, mdiTransitConnection, @@ -69,6 +70,7 @@ import type { HaMenu } from "../../../components/ha-menu"; import "../../../components/ha-menu-item"; import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; +import { createAreaRegistryEntry } from "../../../data/area_registry"; import { AutomationEntity, deleteAutomation, @@ -106,6 +108,7 @@ import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route, ServiceCallResponse } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity"; +import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { configSections } from "../ha-panel-config"; @@ -403,6 +406,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { ${this.hass.localize("ui.panel.config.category.editor.add")} `; + const labelItems = html`${this._labels?.map((label) => { const color = label.color ? computeCssColor(label.color) : undefined; const selected = this._selected.every((entityId) => @@ -440,10 +444,45 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { `; - const labelsInOverflow = - (this._sizeController.value && this._sizeController.value < 700) || + const areaItems = html`${Object.values(this.hass.areas).map( + (area) => + html` + ${area.icon + ? html`` + : html``} +
${area.name}
+
` + )} + +
+ ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.no_area" + )} +
+
+ + +
+ ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.add_area" + )} +
+
`; + + const areasInOverflow = + (this._sizeController.value && this._sizeController.value < 900) || (!this._sizeController.value && this.hass.dockedSidebar === "docked"); + const labelsInOverflow = + areasInOverflow && + (!this._sizeController.value || this._sizeController.value < 700); + const automations = this._automations( this.automations, this._entityReg, @@ -598,6 +637,22 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { > ${labelItems} + `} + ${areasInOverflow + ? nothing + : html` + + + + ${areaItems} `}` : nothing } @@ -662,6 +717,24 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { ` : nothing } + ${ + this.narrow || areasInOverflow + ? html` + +
+ ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.move_area" + )} +
+ +
+ ${areaItems} +
` + : nothing + }
@@ -1191,6 +1264,46 @@ ${rejected } } + private async _handleBulkArea(ev) { + const area = ev.currentTarget.value; + this._bulkAddArea(area); + } + + private async _bulkAddArea(area: string) { + const promises: Promise[] = []; + this._selected.forEach((entityId) => { + promises.push( + updateEntityRegistryEntry(this.hass, entityId, { + area_id: area, + }) + ); + }); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } + } + + private async _bulkCreateArea() { + showAreaRegistryDetailDialog(this, { + createEntry: async (values) => { + const area = await createAreaRegistryEntry(this.hass, values); + this._bulkAddArea(area.area_id); + return area; + }, + }); + } + private async _handleBulkEnable() { const promises: Promise[] = []; this._selected.forEach((entityId) => { diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 1e8d9a6db479..2260effc44ab 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -53,6 +53,7 @@ import "../../../components/ha-alert"; import "../../../components/ha-button-menu"; import "../../../components/ha-check-list-item"; import "../../../components/ha-filter-devices"; +import "../../../components/ha-filter-domains"; import "../../../components/ha-filter-floor-areas"; import "../../../components/ha-filter-integrations"; import "../../../components/ha-filter-labels"; @@ -443,6 +444,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { entryIds.includes(entity.config_entry_id)) ); filter.value!.forEach((domain) => filteredDomains.add(domain)); + } else if (key === "ha-filter-domains" && filter.value?.length) { + filteredEntities = filteredEntities.filter((entity) => + filter.value?.includes(computeDomain(entity.entity_id)) + ); } else if (key === "ha-filter-labels" && filter.value?.length) { filteredEntities = filteredEntities.filter((entity) => entity.labels.some((lbl) => filter.value!.includes(lbl)) @@ -782,6 +787,15 @@ ${ .narrow=${this.narrow} @expanded-changed=${this._filterExpanded} > + `; + const labelItems = html` ${this._labels?.map((label) => { const color = label.color ? computeCssColor(label.color) : undefined; const selected = this._selected.every((entityId) => @@ -442,9 +446,46 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { ${this.hass.localize("ui.panel.config.labels.add_label")}
`; - const labelsInOverflow = - (this._sizeController.value && this._sizeController.value < 700) || + + const areaItems = html`${Object.values(this.hass.areas).map( + (area) => + html` + ${area.icon + ? html`` + : html``} +
${area.name}
+
` + )} + +
+ ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.no_area" + )} +
+
+ + +
+ ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.add_area" + )} +
+
`; + + const areasInOverflow = + (this._sizeController.value && this._sizeController.value < 900) || (!this._sizeController.value && this.hass.dockedSidebar === "docked"); + + const labelsInOverflow = + areasInOverflow && + (!this._sizeController.value || this._sizeController.value < 700); + const scenes = this._scenes( this.scenes, this._entityReg, @@ -453,6 +494,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { this._labels, this._filteredScenes ); + return html` ${labelItems} + `} + ${areasInOverflow + ? nothing + : html` + + + + ${areaItems} `}` : nothing} - ${this.narrow || labelsInOverflow + ${this.narrow || areasInOverflow ? html` ${ @@ -630,8 +688,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { : nothing } ${ - this.narrow || this.hass.dockedSidebar === "docked" - ? html` + this.narrow || labelsInOverflow + ? html`
${this.hass.localize( @@ -647,6 +705,24 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { ` : nothing } + ${ + this.narrow || areasInOverflow + ? html` + +
+ ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.move_area" + )} +
+ +
+ ${areaItems} +
` + : nothing + } ` : nothing} ${!this.scenes.length @@ -875,6 +951,46 @@ ${rejected } } + private async _handleBulkArea(ev) { + const area = ev.currentTarget.value; + this._bulkAddArea(area); + } + + private async _bulkAddArea(area: string) { + const promises: Promise[] = []; + this._selected.forEach((entityId) => { + promises.push( + updateEntityRegistryEntry(this.hass, entityId, { + area_id: area, + }) + ); + }); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } + } + + private async _bulkCreateArea() { + showAreaRegistryDetailDialog(this, { + createEntry: async (values) => { + const area = await createAreaRegistryEntry(this.hass, values); + this._bulkAddArea(area.area_id); + return area; + }, + }); + } + private _editCategory(scene: any) { const entityReg = this._entityReg.find( (reg) => reg.entity_id === scene.entity_id diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 2d8f5e70c114..c2435b6ac90e 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -13,6 +13,7 @@ import { mdiPlus, mdiScriptText, mdiTag, + mdiTextureBox, mdiTransitConnection, } from "@mdi/js"; import { differenceInDays } from "date-fns"; @@ -61,6 +62,7 @@ import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-menu-item"; import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; +import { createAreaRegistryEntry } from "../../../data/area_registry"; import { CategoryRegistryEntry, createCategoryRegistryEntry, @@ -98,6 +100,7 @@ import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { showToast } from "../../../util/toast"; +import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail"; import { showNewAutomationDialog } from "../automation/show-dialog-new-automation"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; @@ -418,6 +421,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { ${this.hass.localize("ui.panel.config.category.editor.add")}
`; + const labelItems = html`${this._labels?.map((label) => { const color = label.color ? computeCssColor(label.color) : undefined; const selected = this._selected.every((entityId) => @@ -454,9 +458,46 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { ${this.hass.localize("ui.panel.config.labels.add_label")} `; - const labelsInOverflow = - (this._sizeController.value && this._sizeController.value < 700) || + + const areaItems = html`${Object.values(this.hass.areas).map( + (area) => + html` + ${area.icon + ? html`` + : html``} +
${area.name}
+
` + )} + +
+ ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.no_area" + )} +
+
+ + +
+ ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.add_area" + )} +
+
`; + + const areasInOverflow = + (this._sizeController.value && this._sizeController.value < 900) || (!this._sizeController.value && this.hass.dockedSidebar === "docked"); + + const labelsInOverflow = + areasInOverflow && + (!this._sizeController.value || this._sizeController.value < 700); + const scripts = this._scripts( this.scripts, this._entityReg, @@ -608,9 +649,25 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { > ${labelItems} +
`} + ${areasInOverflow + ? nothing + : html` + + + + ${areaItems} `}` : nothing} - ${this.narrow || labelsInOverflow + ${this.narrow || areasInOverflow ? html` ${ @@ -656,8 +713,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { : nothing } ${ - this.narrow || this.hass.dockedSidebar === "docked" - ? html` + this.narrow || labelsInOverflow + ? html`
${this.hass.localize( @@ -673,6 +730,24 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { ` : nothing } + ${ + this.narrow || areasInOverflow + ? html` + +
+ ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.move_area" + )} +
+ +
+ ${areaItems} +
` + : nothing + } ` : nothing} ${!this.scripts.length @@ -1111,6 +1186,46 @@ ${rejected }); } + private async _handleBulkArea(ev) { + const area = ev.currentTarget.value; + this._bulkAddArea(area); + } + + private async _bulkAddArea(area: string) { + const promises: Promise[] = []; + this._selected.forEach((entityId) => { + promises.push( + updateEntityRegistryEntry(this.hass, entityId, { + area_id: area, + }) + ); + }); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } + } + + private async _bulkCreateArea() { + showAreaRegistryDetailDialog(this, { + createEntry: async (values) => { + const area = await createAreaRegistryEntry(this.hass, values); + this._bulkAddArea(area.area_id); + return area; + }, + }); + } + private _handleSortingChanged(ev: CustomEvent) { this._activeSorting = ev.detail; } diff --git a/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts b/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts index 4b13e4432338..7dabb1a3ad06 100644 --- a/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts @@ -90,12 +90,10 @@ class HuiLockOpenDoorCardFeature return html` ${this._buttonState === "success" ? html` -
-

- - ${this.hass.localize("ui.card.lock.open_door_success")} -

-
+

+ + ${this.hass.localize("ui.card.lock.open_door_success")} +

` : html` @@ -115,12 +113,6 @@ class HuiLockOpenDoorCardFeature static get styles(): CSSResultGroup { return css` - .buttons { - display: flex; - align-items: center; - justify-content: center; - margin-top: 0; - } ha-control-button { font-size: 14px; } @@ -139,10 +131,14 @@ class HuiLockOpenDoorCardFeature line-height: 14px; display: flex; align-items: center; + justify-content: center; flex-direction: row; gap: 8px; font-weight: 500; color: var(--success-color); + margin: 0 12px 12px 12px; + height: 40px; + text-align: center; } ha-control-button-group + ha-attributes:not([empty]) { margin-top: 16px; diff --git a/src/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index 6d3d49b401c6..65189e351360 100644 --- a/src/panels/lovelace/cards/hui-map-card.ts +++ b/src/panels/lovelace/cards/hui-map-card.ts @@ -138,7 +138,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { includeDomains ); - return { type: "map", entities: foundEntities }; + return { type: "map", entities: foundEntities, theme_mode: "auto" }; } protected render() { @@ -151,6 +151,17 @@ class HuiMapCard extends LitElement implements LovelaceCard { (${this._error.code}) `; } + + const isDarkMode = + this._config.dark_mode || this._config.theme_mode === "dark" + ? true + : this._config.theme_mode === "light" + ? false + : this.hass.themes.darkMode; + + const themeMode = + this._config.theme_mode || (this._config.dark_mode ? "dark" : "auto"); + return html`
@@ -161,7 +172,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { .paths=${this._getHistoryPaths(this._config, this._stateHistory)} .autoFit=${this._config.auto_fit || false} .fitZones=${this._config.fit_zones} - ?darkMode=${this._config.dark_mode} + .themeMode=${themeMode} interactiveZones renderPassive > @@ -170,6 +181,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { "ui.panel.lovelace.cards.map.reset_focus" )} .path=${mdiImageFilterCenterFocus} + style=${isDarkMode ? "color:#ffffff" : "color:#000000"} @click=${this._fitMap} tabindex="0" > diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 0ae62e6aea78..a32d345c2aee 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -3,7 +3,7 @@ import { ActionConfig } from "../../../data/lovelace/config/action"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { Statistic, StatisticType } from "../../../data/recorder"; import { ForecastType } from "../../../data/weather"; -import { FullCalendarView, TranslationDict } from "../../../types"; +import { FullCalendarView, ThemeMode, TranslationDict } from "../../../types"; import { LovelaceCardFeatureConfig } from "../card-features/types"; import { LegacyStateFilter } from "../common/evaluate-filter"; import { Condition, LegacyCondition } from "../common/validate-condition"; @@ -314,6 +314,7 @@ export interface MapCardConfig extends LovelaceCardConfig { hours_to_show?: number; geo_location_sources?: string[]; dark_mode?: boolean; + theme_mode?: ThemeMode; } export interface MarkdownCardConfig extends LovelaceCardConfig { diff --git a/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts index 7fb605e3f885..636ed4b85b5a 100644 --- a/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts @@ -1,3 +1,4 @@ +import { mdiPalette } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { @@ -11,6 +12,7 @@ import { string, union, } from "superstruct"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; import { hasLocation } from "../../../../common/entity/has_location"; import "../../../../components/ha-form/ha-form"; @@ -28,6 +30,7 @@ import { processEditorEntities } from "../process-editor-entities"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; +import { LocalizeFunc } from "../../../../common/translations/localize"; export const mapEntitiesConfigStruct = union([ object({ @@ -50,30 +53,11 @@ const cardConfigStruct = assign( hours_to_show: optional(number()), geo_location_sources: optional(array(string())), auto_fit: optional(boolean()), + theme_mode: optional(string()), }) ); -const SCHEMA = [ - { name: "title", selector: { text: {} } }, - { - name: "", - type: "grid", - schema: [ - { name: "aspect_ratio", selector: { text: {} } }, - { - name: "default_zoom", - default: DEFAULT_ZOOM, - selector: { number: { mode: "box", min: 0 } }, - }, - { name: "dark_mode", selector: { boolean: {} } }, - { - name: "hours_to_show", - default: DEFAULT_HOURS_TO_SHOW, - selector: { number: { mode: "box", min: 0 } }, - }, - ], - }, -] as const; +const themeModes = ["auto", "light", "dark"] as const; @customElement("hui-map-card-editor") export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { @@ -83,8 +67,68 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { @state() private _configEntities?: EntityConfig[]; + private _schema = memoizeOne( + (localize: LocalizeFunc) => + [ + { name: "title", selector: { text: {} } }, + { + name: "", + type: "expandable", + iconPath: mdiPalette, + title: localize(`ui.panel.lovelace.editor.card.map.appearance`), + schema: [ + { + name: "", + type: "grid", + schema: [ + { name: "aspect_ratio", selector: { text: {} } }, + { + name: "default_zoom", + default: DEFAULT_ZOOM, + selector: { number: { mode: "box", min: 0 } }, + }, + { + name: "theme_mode", + default: "auto", + selector: { + select: { + mode: "dropdown", + options: themeModes.map((themeMode) => ({ + value: themeMode, + label: localize( + `ui.panel.lovelace.editor.card.map.theme_modes.${themeMode}` + ), + })), + }, + }, + }, + { + name: "hours_to_show", + default: DEFAULT_HOURS_TO_SHOW, + selector: { number: { mode: "box", min: 0 } }, + }, + ], + }, + ], + }, + ] as const + ); + public setConfig(config: MapCardConfig): void { assert(config, cardConfigStruct); + + // Migrate legacy dark_mode to theme_mode + if (!this._config && !("theme_mode" in config)) { + config = { ...config }; + if (config.dark_mode) { + config.theme_mode = "dark"; + } else { + config.theme_mode = "auto"; + } + delete config.dark_mode; + fireEvent(this, "config-changed", { config: config }); + } + this._config = config; this._configEntities = config.entities ? processEditorEntities(config.entities) @@ -104,33 +148,32 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { -
- -

- ${this.hass.localize( - "ui.panel.lovelace.editor.card.map.geo_location_sources" - )} -

-
- -
-
+ + + +

+ ${this.hass.localize( + "ui.panel.lovelace.editor.card.map.geo_location_sources" + )} +

+ + `; } @@ -170,9 +213,14 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { fireEvent(this, "config-changed", { config: ev.detail.value }); } - private _computeLabelCallback = (schema: SchemaUnion) => { + private _computeLabelCallback = ( + schema: SchemaUnion> + ) => { switch (schema.name) { - case "dark_mode": + case "theme_mode": + return this.hass!.localize( + `ui.panel.lovelace.editor.card.map.${schema.name}` + ); case "default_zoom": return this.hass!.localize( `ui.panel.lovelace.editor.card.map.${schema.name}` @@ -185,16 +233,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { }; static get styles(): CSSResultGroup { - return [ - configElementStyle, - css` - .geo_location_sources { - padding-left: 20px; - padding-inline-start: 20px; - direction: var(--direction); - } - `, - ]; + return [configElementStyle, css``]; } } diff --git a/src/translations/en.json b/src/translations/en.json index 65b153f5221d..8be6f084acee 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5835,7 +5835,14 @@ "name": "Map", "geo_location_sources": "Geolocation sources", "dark_mode": "Dark mode?", - "default_zoom": "Default zoom", + "appearance": "Appearance", + "theme_mode": "Theme Mode", + "theme_modes": { + "auto": "Auto", + "light": "Light", + "dark": "Dark" + }, + "default_zoom": "Default Zoom", "source": "Source", "description": "The Map card that allows you to display entities on a map." }, diff --git a/src/types.ts b/src/types.ts index c6b32be85d06..31a412364034 100644 --- a/src/types.ts +++ b/src/types.ts @@ -139,6 +139,8 @@ export type FullCalendarView = | "dayGridDay" | "listWeek"; +export type ThemeMode = "auto" | "light" | "dark"; + export interface ToggleButton { label: string; iconPath?: string; diff --git a/yarn.lock b/yarn.lock index b546d2a82f0d..51eb5d603955 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7028,10 +7028,10 @@ __metadata: languageName: node linkType: hard -"cropperjs@npm:1.6.1": - version: 1.6.1 - resolution: "cropperjs@npm:1.6.1" - checksum: 10/3ecd895ba8820021d6efd8effda54fb40a418e6940c307d46c62812d7d0ee10aacde473b44b7cfb315ec0fc2e5c5c573f4de06e8c2e8b7cb134f12f65c1d4aa7 +"cropperjs@npm:1.6.2": + version: 1.6.2 + resolution: "cropperjs@npm:1.6.2" + checksum: 10/4b97ac27b7fd65316a531372dd96ce1073b4620c1b012fbad5acb0fdc28952112466bc333a98c510fe2bc9dcdeee58f5c8619522c045c6166300621673e3d759 languageName: node linkType: hard @@ -9674,7 +9674,7 @@ __metadata: color-name: "npm:2.0.0" comlink: "npm:4.4.1" core-js: "npm:3.37.0" - cropperjs: "npm:1.6.1" + cropperjs: "npm:1.6.2" date-fns: "npm:3.6.0" date-fns-tz: "npm:3.1.3" deep-clone-simple: "npm:1.1.1"