diff --git a/src/components/map/ha-locations-editor.ts b/src/components/map/ha-locations-editor.ts index e0f1c1d8f308..b2c63c394c0a 100644 --- a/src/components/map/ha-locations-editor.ts +++ b/src/components/map/ha-locations-editor.ts @@ -133,7 +133,7 @@ export class HaLocationsEditor extends LitElement { .layers=${this._getLayers(this._circles, this._locationMarkers)} .zoom=${this.zoom} .autoFit=${this.autoFit} - ?darkMode=${this.darkMode} + ?forceDarkMode=${this.darkMode} > ${this.helper ? html`${this.helper}` diff --git a/src/components/map/ha-map.ts b/src/components/map/ha-map.ts index 1fcbceace41b..c4704dc798db 100644 --- a/src/components/map/ha-map.ts +++ b/src/components/map/ha-map.ts @@ -69,7 +69,9 @@ export class HaMap extends ReactiveElement { @property({ type: Boolean }) public fitZones = false; - @property({ type: Boolean }) public darkMode = false; + @property({ type: Boolean }) public forceDarkMode = false; + + @property({ type: Boolean }) public forceLightMode = false; @property({ type: Number }) public zoom = 14; @@ -154,7 +156,8 @@ export class HaMap extends ReactiveElement { } if ( - !changedProps.has("darkMode") && + !changedProps.has("forceDarkMode") && + !changedProps.has("forceLightMode") && (!changedProps.has("hass") || (oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode)) ) { @@ -164,11 +167,13 @@ export class HaMap extends ReactiveElement { } private _updateMapStyle(): void { - const darkMode = this.darkMode || (this.hass.themes.darkMode ?? false); - const forcedDark = this.darkMode; + const darkMode = + !this.forceLightMode && + (this.forceDarkMode || (this.hass.themes.darkMode ?? false)); const map = this.renderRoot.querySelector("#map"); map!.classList.toggle("dark", darkMode); - map!.classList.toggle("forced-dark", forcedDark); + map!.classList.toggle("forced-dark", this.forceDarkMode); + map!.classList.toggle("forced-light", this.forceLightMode); } private async _loadMap(): Promise { @@ -398,8 +403,13 @@ export class HaMap extends ReactiveElement { "--dark-primary-color" ); - const className = - this.darkMode || this.hass.themes.darkMode ? "dark" : "light"; + const className = this.forceLightMode + ? "light" + : this.forceDarkMode + ? "dark" + : this.hass.themes.darkMode + ? "dark" + : "light"; for (const entity of this.entities) { const stateObj = hass.states[getEntityId(entity)]; @@ -543,27 +553,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/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index 6d3d49b401c6..c0b769b81840 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,14 @@ 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; + return html`
@@ -161,7 +169,9 @@ 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} + ?forceDarkMode=${this._config.theme_mode === "dark" || + this._config.dark_mode} + ?forceLightMode=${this._config.theme_mode === "light"} interactiveZones renderPassive > @@ -170,6 +180,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;