From 60079ce9993371ef6942364b48e94b815b18ca12 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 28 Jan 2026 18:46:22 +0100 Subject: [PATCH] Add welcome banner for new overview dashboard (#29223) --- ...svg => icon-dashboard-overview-legacy.svg} | 0 ...svg => icon-dashboard-overview-legacy.svg} | 0 src/data/frontend.ts | 3 + src/data/panel.ts | 20 ++- src/onboarding/ha-onboarding.ts | 6 + .../config/dashboard/dialog-new-dashboard.ts | 16 +-- .../home/dialogs/dialog-new-overview.ts | 132 ++++++++++++++++++ .../home/dialogs/show-dialog-new-overview.ts | 18 +++ src/panels/home/ha-panel-home.ts | 127 ++++++++++++++++- src/panels/lovelace/hui-root.ts | 5 +- src/translations/en.json | 18 +-- 11 files changed, 313 insertions(+), 32 deletions(-) rename public/static/images/dashboard-options/dark/{icon-dashboard-overview.svg => icon-dashboard-overview-legacy.svg} (100%) rename public/static/images/dashboard-options/light/{icon-dashboard-overview.svg => icon-dashboard-overview-legacy.svg} (100%) create mode 100644 src/panels/home/dialogs/dialog-new-overview.ts create mode 100644 src/panels/home/dialogs/show-dialog-new-overview.ts diff --git a/public/static/images/dashboard-options/dark/icon-dashboard-overview.svg b/public/static/images/dashboard-options/dark/icon-dashboard-overview-legacy.svg similarity index 100% rename from public/static/images/dashboard-options/dark/icon-dashboard-overview.svg rename to public/static/images/dashboard-options/dark/icon-dashboard-overview-legacy.svg diff --git a/public/static/images/dashboard-options/light/icon-dashboard-overview.svg b/public/static/images/dashboard-options/light/icon-dashboard-overview-legacy.svg similarity index 100% rename from public/static/images/dashboard-options/light/icon-dashboard-overview.svg rename to public/static/images/dashboard-options/light/icon-dashboard-overview-legacy.svg diff --git a/src/data/frontend.ts b/src/data/frontend.ts index 7746cefb38..26957d0b29 100644 --- a/src/data/frontend.ts +++ b/src/data/frontend.ts @@ -13,10 +13,13 @@ export interface SidebarFrontendUserData { export interface CoreFrontendSystemData { default_panel?: string; + onboarded_version?: string; + onboarded_date?: string; } export interface HomeFrontendSystemData { favorite_entities?: string[]; + welcome_banner_dismissed?: boolean; } declare global { diff --git a/src/data/panel.ts b/src/data/panel.ts index 20be01ebe4..fe55d4b719 100644 --- a/src/data/panel.ts +++ b/src/data/panel.ts @@ -15,16 +15,26 @@ import type { LocalizeKeys } from "../common/translations/localize"; /** Panel to show when no panel is picked. */ export const DEFAULT_PANEL = "home"; +export const hasLegacyOverviewPanel = (hass: HomeAssistant): boolean => + Boolean(hass.panels.lovelace?.config); + export const getLegacyDefaultPanelUrlPath = (): string | null => { const defaultPanel = window.localStorage.getItem("defaultPanel"); return defaultPanel ? JSON.parse(defaultPanel) : null; }; -export const getDefaultPanelUrlPath = (hass: HomeAssistant): string => - hass.userData?.default_panel || - hass.systemData?.default_panel || - getLegacyDefaultPanelUrlPath() || - DEFAULT_PANEL; +export const getDefaultPanelUrlPath = (hass: HomeAssistant): string => { + const defaultPanel = + hass.userData?.default_panel || + hass.systemData?.default_panel || + getLegacyDefaultPanelUrlPath() || + DEFAULT_PANEL; + // If default panel is lovelace and no old overview exists, fall back to home + if (defaultPanel === "lovelace" && !hasLegacyOverviewPanel(hass)) { + return DEFAULT_PANEL; + } + return defaultPanel; +}; export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => { const panel = getDefaultPanelUrlPath(hass); diff --git a/src/onboarding/ha-onboarding.ts b/src/onboarding/ha-onboarding.ts index c3fed7f574..ce19271d23 100644 --- a/src/onboarding/ha-onboarding.ts +++ b/src/onboarding/ha-onboarding.ts @@ -25,6 +25,7 @@ import { subscribeOne } from "../common/util/subscribe-one"; import "../components/ha-card"; import type { AuthUrlSearchParams } from "../data/auth"; import { hassUrl } from "../data/auth"; +import { saveFrontendSystemData } from "../data/frontend"; import type { OnboardingResponses, OnboardingStep } from "../data/onboarding"; import { fetchInstallationType, @@ -406,6 +407,11 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { ), }; + await saveFrontendSystemData(this.hass!.connection, "core", { + onboarded_version: this.hass!.config.version, + onboarded_date: new Date().toISOString(), + }); + let result: OnboardingResponses["integration"]; try { diff --git a/src/panels/config/dashboard/dialog-new-dashboard.ts b/src/panels/config/dashboard/dialog-new-dashboard.ts index 8045856baa..87fa41bd26 100644 --- a/src/panels/config/dashboard/dialog-new-dashboard.ts +++ b/src/panels/config/dashboard/dialog-new-dashboard.ts @@ -28,15 +28,15 @@ interface Strategy { const STRATEGIES = [ { - type: "overview", + type: "original-states", images: { light: - "/static/images/dashboard-options/light/icon-dashboard-overview.svg", - dark: "/static/images/dashboard-options/dark/icon-dashboard-overview.svg", + "/static/images/dashboard-options/light/icon-dashboard-overview-legacy.svg", + dark: "/static/images/dashboard-options/dark/icon-dashboard-overview-legacy.svg", }, - name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview.title", + name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview-legacy.title", description: - "ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview.description", + "ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview-legacy.description", }, { type: "map", @@ -244,11 +244,7 @@ class DialogNewDashboard extends LitElement implements HassDialog { if (target.config) { config = target.config; } else if (target.strategy) { - if (target.strategy === "overview") { - config = null; - } else { - config = this._generateStrategyConfig(target.strategy); - } + config = this._generateStrategyConfig(target.strategy); } this._params?.selectConfig(config); diff --git a/src/panels/home/dialogs/dialog-new-overview.ts b/src/panels/home/dialogs/dialog-new-overview.ts new file mode 100644 index 0000000000..1e6e12d9fa --- /dev/null +++ b/src/panels/home/dialogs/dialog-new-overview.ts @@ -0,0 +1,132 @@ +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-button"; +import "../../../components/ha-dialog-footer"; +import "../../../components/ha-wa-dialog"; +import type { HassDialog } from "../../../dialogs/make-dialog-manager"; +import { haStyleDialog } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import type { NewOverviewDialogParams } from "./show-dialog-new-overview"; + +@customElement("dialog-new-overview") +export class DialogNewOverview + extends LitElement + implements HassDialog +{ + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: NewOverviewDialogParams; + + @state() private _open = false; + + public showDialog(params: NewOverviewDialogParams): void { + this._params = params; + this._open = true; + } + + public closeDialog(): boolean { + this._open = false; + return true; + } + + private _dialogClosed(): void { + if (this._params) { + this._params.dismiss(); + } + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render() { + if (!this._params) { + return nothing; + } + + return html` + +
+

+ The overview dashboard has been redesigned to give you a better + experience managing your smart home. +

+

What's new

+
    +
  • + Automatic organization - Your devices are now + automatically organized by area and floor. +
  • +
  • + Favorites - Pin your most used entities to the + top for quick access. +
  • +
+

Your existing dashboards

+

+ Your manual dashboards are still available in the sidebar. This new + overview works alongside them. You can also create a new dashboard + using the "Overview (legacy)" template in + dashboard settings. +

+
+ + + OK, understood + + +
+ `; + } + + static styles = [ + haStyleDialog, + css` + ha-wa-dialog { + --dialog-content-padding: var(--ha-space-6); + } + + .content { + line-height: var(--ha-line-height-normal); + } + + p { + margin: 0 0 var(--ha-space-4) 0; + color: var(--secondary-text-color); + } + + h3 { + margin: var(--ha-space-4) 0 var(--ha-space-2) 0; + font-size: var(--ha-font-size-l); + font-weight: var(--ha-font-weight-medium); + } + + ul { + margin: 0 0 var(--ha-space-4) 0; + padding-left: var(--ha-space-6); + color: var(--secondary-text-color); + } + + li { + margin-bottom: var(--ha-space-2); + } + + li strong { + color: var(--primary-text-color); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-new-overview": DialogNewOverview; + } +} diff --git a/src/panels/home/dialogs/show-dialog-new-overview.ts b/src/panels/home/dialogs/show-dialog-new-overview.ts new file mode 100644 index 0000000000..909f79651f --- /dev/null +++ b/src/panels/home/dialogs/show-dialog-new-overview.ts @@ -0,0 +1,18 @@ +import { fireEvent } from "../../../common/dom/fire_event"; + +export interface NewOverviewDialogParams { + dismiss: () => void; +} + +export const loadNewOverviewDialog = () => import("./dialog-new-overview"); + +export const showNewOverviewDialog = ( + element: HTMLElement, + params: NewOverviewDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-new-overview", + dialogImport: loadNewOverviewDialog, + dialogParams: params, + }); +}; diff --git a/src/panels/home/ha-panel-home.ts b/src/panels/home/ha-panel-home.ts index d9a0031287..a28085e0d1 100644 --- a/src/panels/home/ha-panel-home.ts +++ b/src/panels/home/ha-panel-home.ts @@ -1,10 +1,15 @@ +import { ResizeController } from "@lit-labs/observers/resize-controller"; import { mdiPencil } from "@mdi/js"; import type { CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { atLeastVersion } from "../../common/config/version"; import { navigate } from "../../common/navigate"; import { debounce } from "../../common/util/debounce"; import { deepEqual } from "../../common/util/deep-equal"; +import "../../components/ha-button"; +import "../../components/ha-svg-icon"; import { updateAreaRegistryEntry } from "../../data/area/area_registry"; import { updateDeviceRegistryEntry } from "../../data/device/device_registry"; import { @@ -13,6 +18,7 @@ import { type HomeFrontendSystemData, } from "../../data/frontend"; import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types"; +import { mdiHomeAssistant } from "../../resources/home-assistant-logo-svg"; import type { HomeAssistant, PanelInfo, Route } from "../../types"; import { showToast } from "../../util/toast"; import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail"; @@ -23,6 +29,8 @@ import type { ExtraActionItem } from "../lovelace/hui-root"; import { expandLovelaceConfigStrategies } from "../lovelace/strategies/get-strategy"; import type { Lovelace } from "../lovelace/types"; import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home"; +import { showNewOverviewDialog } from "./dialogs/show-dialog-new-overview"; +import { hasLegacyOverviewPanel } from "../../data/panel"; @customElement("ha-panel-home") class PanelHome extends LitElement { @@ -40,6 +48,31 @@ class PanelHome extends LitElement { @state() private _extraActionItems?: ExtraActionItem[]; + private get _showBanner(): boolean { + // Don't show if already dismissed + if (this._config.welcome_banner_dismissed) { + return false; + } + // Don't show if HA is not running + if (this.hass.config.state !== "RUNNING") { + return false; + } + // Show banner only for users who: + // 1. Were onboarded before 2026.2 (or have no onboarded_version) + // 2. Don't have a custom "lovelace" dashboard (old overview) + const onboardedVersion = this.hass.systemData?.onboarded_version; + const isNewInstance = + onboardedVersion && atLeastVersion(onboardedVersion, 2026, 2); + const hasOldOverview = hasLegacyOverviewPanel(this.hass); + return !isNewInstance && !hasOldOverview; + } + + private _bannerHeight = new ResizeController(this, { + target: null, + callback: (entries) => + (entries[0]?.target as HTMLElement | undefined)?.offsetHeight ?? 0, + }); + public willUpdate(changedProps: PropertyValues) { super.willUpdate(changedProps); // Initial setup @@ -80,7 +113,7 @@ class PanelHome extends LitElement { this.hass.config.state === "RUNNING" && oldHass.config.state !== "RUNNING" ) { - this._setLovelace(); + this._setup(); } } } @@ -211,7 +244,14 @@ class PanelHome extends LitElement { return nothing; } + const huiRootStyle = styleMap({ + "--view-container-padding-top": this._bannerHeight.value + ? `${this._bannerHeight.value}px` + : undefined, + }); + return html` + ${this._renderBanner()} `; } + private _renderBanner() { + if (!this._showBanner) { + return nothing; + } + + return html` + + `; + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (changedProps.has("_showBanner") || changedProps.has("_lovelace")) { + const banner = this.shadowRoot?.querySelector(".banner"); + if (banner) { + this._bannerHeight.observe(banner); + } + } + } + + private _learnMore() { + showNewOverviewDialog(this, { + dismiss: async () => { + const newConfig = { + ...this._config, + welcome_banner_dismissed: true, + }; + this._config = newConfig; + await saveFrontendSystemData(this.hass.connection, "home", newConfig); + }, + }); + } + private async _setLovelace() { const strategyConfig: LovelaceDashboardStrategyConfig = { strategy: { @@ -282,6 +368,45 @@ class PanelHome extends LitElement { :host { display: block; } + .banner { + display: flex; + align-items: center; + flex-wrap: wrap; + padding: var(--ha-space-2) var(--ha-space-4); + background-color: var(--primary-color); + color: var(--text-primary-color); + gap: var(--ha-space-2); + position: fixed; + top: var(--header-height, 56px); + left: var(--mdc-drawer-width, 0px); + right: 0; + z-index: 5; + } + .banner-content { + display: flex; + align-items: center; + gap: var(--ha-space-2); + flex: 1; + min-width: 200px; + } + .banner ha-svg-icon { + --mdc-icon-size: 24px; + flex-shrink: 0; + } + .banner-text { + font-size: 14px; + font-weight: 500; + } + .banner-actions { + display: flex; + flex: none; + gap: var(--ha-space-2); + align-items: center; + margin-inline-start: auto; + } + .banner-actions ha-button::part(base) { + text-wrap: nowrap; + } `; } diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 1b74de0d78..5bbf5330e9 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -1497,7 +1497,10 @@ class HUIRoot extends LitElement { display: flex; min-height: 100vh; box-sizing: border-box; - padding-top: calc(var(--header-height) + var(--safe-area-inset-top)); + padding-top: calc( + var(--header-height) + var(--safe-area-inset-top) + + var(--view-container-padding-top, 0px) + ); padding-right: var(--safe-area-inset-right); padding-inline-end: var(--safe-area-inset-right); padding-bottom: calc( diff --git a/src/translations/en.json b/src/translations/en.json index 6246ddabd3..8ac0a166ab 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -14,7 +14,7 @@ "light": "Lights", "security": "Security", "climate": "Climate", - "home": "Home" + "home": "Overview" }, "state": { "default": { @@ -4155,21 +4155,9 @@ "title": "Webpage", "description": "Integrate a webpage as a dashboard" }, - "areas": { - "title": "Areas (experimental)", - "description": "Display your devices with a view for each area" - }, - "default": { - "title": "Default dashboard", - "description": "Display your devices grouped by area" - }, - "overview": { - "title": "Overview", + "overview-legacy": { + "title": "Overview (Legacy)", "description": "Gives an overview of all your entities and areas they are in" - }, - "home": { - "title": "Home (experimental)", - "description": "Global overview of your home" } }, "search_dashboards": "Search dashboards",