From 55c74d7959735cd169be41b57a0ad4abd55c8de6 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 22 Jan 2026 13:15:27 +0100 Subject: [PATCH] Add empty state to Home panel strategies (#29113) --- src/panels/home/ha-panel-home.ts | 24 ++++- .../lovelace/cards/hui-empty-state-card.ts | 58 ++++++++++-- src/panels/lovelace/cards/types.ts | 12 ++- .../hui-empty-state-card-editor.ts | 94 +++++++++++++++---- .../home/home-area-view-strategy.ts | 38 +++++++- .../home/home-dashboard-strategy.ts | 2 + .../home/home-other-devices-view-strategy.ts | 11 ++- .../home/home-overview-view-strategy.ts | 55 +++++++++++ .../original-states-view-strategy.ts | 21 +++-- src/translations/en.json | 24 +++-- 10 files changed, 283 insertions(+), 56 deletions(-) diff --git a/src/panels/home/ha-panel-home.ts b/src/panels/home/ha-panel-home.ts index 28379d7307..d9a0031287 100644 --- a/src/panels/home/ha-panel-home.ts +++ b/src/panels/home/ha-panel-home.ts @@ -15,12 +15,13 @@ import { import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types"; import type { HomeAssistant, PanelInfo, Route } from "../../types"; import { showToast } from "../../util/toast"; +import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail"; +import { showDeviceRegistryDetailDialog } from "../config/devices/device-registry-detail/show-dialog-device-registry-detail"; +import { showAddIntegrationDialog } from "../config/integrations/show-add-integration-dialog"; import "../lovelace/hui-root"; import type { ExtraActionItem } from "../lovelace/hui-root"; import { expandLovelaceConfigStrategies } from "../lovelace/strategies/get-strategy"; import type { Lovelace } from "../lovelace/types"; -import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail"; -import { showDeviceRegistryDetailDialog } from "../config/devices/device-registry-detail/show-dialog-device-registry-detail"; import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home"; @customElement("ha-panel-home") @@ -172,13 +173,26 @@ class PanelHome extends LitElement { private _handleLLCustomEvent = (ev: Event) => { const detail = (ev as CustomEvent).detail; if (detail.home_panel) { - const { type, device_id } = detail.home_panel; - if (type === "assign_area") { - this._showAssignAreaDialog(device_id); + const { type } = detail.home_panel; + switch (type) { + case "assign_area": { + const { device_id } = detail.home_panel; + this._showAssignAreaDialog(device_id); + break; + } + case "add_integration": { + this._showAddIntegrationDialog(); + break; + } } } }; + private async _showAddIntegrationDialog() { + await this.hass.loadFragmentTranslation("config"); + showAddIntegrationDialog(this, { navigateToResult: false }); + } + private _showAssignAreaDialog(deviceId: string) { const device = this.hass.devices[deviceId]; if (!device) { diff --git a/src/panels/lovelace/cards/hui-empty-state-card.ts b/src/panels/lovelace/cards/hui-empty-state-card.ts index b9a050690a..38a35ec48b 100644 --- a/src/panels/lovelace/cards/hui-empty-state-card.ts +++ b/src/panels/lovelace/cards/hui-empty-state-card.ts @@ -1,6 +1,9 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; +import { styleMap } from "lit/directives/style-map"; +import { ifDefined } from "lit/directives/if-defined"; +import { computeCssColor } from "../../../common/color/compute-color"; import "../../../components/ha-card"; import "../../../components/ha-button"; import "../../../components/ha-icon"; @@ -49,17 +52,44 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard { >
${this._config.icon - ? html`` + ? html` + + ` : nothing} ${this._config.title ? html`

${this._config.title}

` : nothing} ${this._config.content ? html`

${this._config.content}

` : nothing} - ${this._config.tap_action && this._config.action_button_text + ${this._config.buttons?.length ? html` - - ${this._config.action_button_text} - +
+ ${this._config.buttons.map( + (button, index) => html` + + ${button.icon + ? html`` + : nothing} + ${button.text} + + ` + )} +
` : nothing}
@@ -67,9 +97,11 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard { `; } - private _handleAction(): void { - if (this._config?.tap_action && this.hass) { - handleAction(this, this.hass, this._config, "tap"); + private _handleButtonAction(ev: Event): void { + const index = (ev.currentTarget as any).index; + const button = this._config?.buttons?.[index]; + if (this.hass && button) { + handleAction(this, this.hass, { tap_action: button.tap_action }, "tap"); } } @@ -94,8 +126,8 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard { max-width: 640px; margin: 0 auto; } - ha-icon { - --mdc-icon-size: var(--ha-space-12); + .card-icon { + --mdc-icon-size: var(--ha-space-16); color: var(--secondary-text-color); } h1 { @@ -107,6 +139,12 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard { margin: 0; color: var(--secondary-text-color); } + .buttons { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: var(--ha-space-2); + } .content-only { background: none; box-shadow: none; diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 0afc347212..316dcdbc0d 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -57,13 +57,21 @@ export interface ConditionalCardConfig extends LovelaceCardConfig { conditions: (Condition | LegacyCondition)[]; } +export interface EmptyStateButtonConfig { + text: string; + icon?: string; + appearance?: "accent" | "filled" | "outlined" | "plain"; + variant?: "brand" | "neutral" | "success" | "warning" | "danger"; + tap_action: ActionConfig; +} + export interface EmptyStateCardConfig extends LovelaceCardConfig { content_only?: boolean; icon?: string; + icon_color?: string; title?: string; content?: string; - action_button_text?: string; - tap_action?: ActionConfig; + buttons?: EmptyStateButtonConfig[]; } export interface EntityCardConfig extends LovelaceCardConfig { diff --git a/src/panels/lovelace/editor/config-elements/hui-empty-state-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-empty-state-card-editor.ts index e42ea6c46b..0b74aa2db5 100644 --- a/src/panels/lovelace/editor/config-elements/hui-empty-state-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-empty-state-card-editor.ts @@ -1,8 +1,16 @@ -import { mdiGestureTap } from "@mdi/js"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { assert, assign, boolean, object, optional, string } from "superstruct"; +import { + array, + assert, + assign, + boolean, + enums, + object, + optional, + string, +} from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; import type { LocalizeFunc } from "../../../../common/translations/localize"; import "../../../../components/ha-form/ha-form"; @@ -16,15 +24,25 @@ import type { LovelaceCardEditor } from "../../types"; import { actionConfigStruct } from "../structs/action-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; +const buttonStruct = object({ + text: string(), + icon: optional(string()), + appearance: optional(enums(["accent", "filled", "outlined", "plain"])), + variant: optional( + enums(["brand", "neutral", "success", "warning", "danger"]) + ), + tap_action: actionConfigStruct, +}); + const cardConfigStruct = assign( baseLovelaceCardConfig, object({ content_only: optional(boolean()), icon: optional(string()), + icon_color: optional(string()), title: optional(string()), content: optional(string()), - action_button_text: optional(string()), - tap_action: optional(actionConfigStruct), + buttons: optional(array(buttonStruct)), }) ); @@ -70,24 +88,66 @@ export class HuiEmptyStateCardEditor }, }, { name: "icon", selector: { icon: {} } }, + { + name: "icon_color", + selector: { + ui_color: {}, + }, + }, { name: "title", selector: { text: {} } }, { name: "content", selector: { text: { multiline: true } } }, { - name: "interactions", - type: "expandable", - flatten: true, - iconPath: mdiGestureTap, - schema: [ - { name: "action_button_text", selector: { text: {} } }, - { - name: "tap_action", - selector: { - ui_action: { - default_action: "none", + name: "buttons", + selector: { + object: { + multiple: true, + label_field: "text", + fields: { + text: { + selector: { text: {} }, + required: true, + }, + icon: { + selector: { icon: {} }, + }, + appearance: { + selector: { + select: { + options: [ + { value: "accent", label: "Accent" }, + { value: "filled", label: "Filled" }, + { value: "outlined", label: "Outlined" }, + { value: "plain", label: "Plain" }, + ], + mode: "dropdown", + }, + }, + }, + variant: { + selector: { + select: { + options: [ + { value: "brand", label: "Brand" }, + { value: "neutral", label: "Neutral" }, + { value: "success", label: "Success" }, + { value: "warning", label: "Warning" }, + { value: "danger", label: "Danger" }, + ], + mode: "dropdown", + }, + }, + }, + tap_action: { + selector: { + ui_action: { + default_action: "none", + }, + }, + required: true, }, }, }, - ], + }, }, ] as const satisfies readonly HaFormSchema[] ); @@ -134,7 +194,7 @@ export class HuiEmptyStateCardEditor switch (schema.name) { case "style": case "content": - case "action_button_text": + case "buttons": return this.hass!.localize( `ui.panel.lovelace.editor.card.empty_state.${schema.name}` ); diff --git a/src/panels/lovelace/strategies/home/home-area-view-strategy.ts b/src/panels/lovelace/strategies/home/home-area-view-strategy.ts index 78b0a579f1..dd3c2850ec 100644 --- a/src/panels/lovelace/strategies/home/home-area-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-area-view-strategy.ts @@ -27,6 +27,7 @@ import { export interface HomeAreaViewStrategyConfig { type: "home-area"; area?: string; + home_panel?: boolean; } @customElement("home-area-view-strategy") @@ -365,13 +366,46 @@ export class HomeAreaViewStrategy extends ReactiveElement { { type: "empty-state", icon: "mdi:sofa-outline", + icon_color: "primary", content_only: true, title: hass.localize( - "ui.panel.lovelace.strategy.areas.empty_state_title" + "ui.panel.lovelace.strategy.home-area.no_devices_title" ), content: hass.localize( - "ui.panel.lovelace.strategy.areas.empty_state_content" + "ui.panel.lovelace.strategy.home-area.no_devices_content" ), + ...(config.home_panel && hass.user?.is_admin + ? { + buttons: [ + { + icon: "mdi:plus", + text: hass.localize( + "ui.panel.lovelace.strategy.home-area.no_devices_add_device" + ), + appearance: "plain", + variant: "brand", + tap_action: { + action: "fire-dom-event", + home_panel: { + type: "add_integration", + }, + }, + }, + { + icon: "mdi:home-plus", + text: hass.localize( + "ui.panel.lovelace.strategy.home-area.no_devices_assign_device" + ), + appearance: "plain", + variant: "brand", + tap_action: { + action: "navigate", + navigation_path: "/home/other-devices", + }, + }, + ], + } + : {}), } as EmptyStateCardConfig, ], }; diff --git a/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts b/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts index 1e2cc10876..c6b5fc3b1c 100644 --- a/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts @@ -59,6 +59,7 @@ export class HomeDashboardStrategy extends ReactiveElement { strategy: { type: "home-area", area: area.area_id, + home_panel: config.home_panel, } satisfies HomeAreaViewStrategyConfig, }; }); @@ -92,6 +93,7 @@ export class HomeDashboardStrategy extends ReactiveElement { strategy: { type: "home-overview", favorite_entities: config.favorite_entities, + home_panel: config.home_panel, } satisfies HomeOverviewViewStrategyConfig, }, ...areaViews, diff --git a/src/panels/lovelace/strategies/home/home-other-devices-view-strategy.ts b/src/panels/lovelace/strategies/home/home-other-devices-view-strategy.ts index 0f5e9da8e8..e0d1b2d10e 100644 --- a/src/panels/lovelace/strategies/home/home-other-devices-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-other-devices-view-strategy.ts @@ -142,7 +142,7 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement { type: "button", icon: "mdi:home-plus", text: hass.localize( - "ui.panel.lovelace.strategy.other_devices.assign_area" + "ui.panel.lovelace.strategy.home-other-devices.assign_area" ), tap_action: { action: "fire-dom-event", @@ -178,7 +178,7 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement { { type: "heading", heading: hass.localize( - "ui.panel.lovelace.strategy.other_devices.helpers" + "ui.panel.lovelace.strategy.home-other-devices.helpers" ), } satisfies HeadingCardConfig, ...helpersEntities.map((e) => ({ @@ -197,7 +197,7 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement { { type: "heading", heading: hass.localize( - "ui.panel.lovelace.strategy.other_devices.entities" + "ui.panel.lovelace.strategy.home-other-devices.entities" ), } satisfies HeadingCardConfig, ...otherEntities.map((e) => ({ @@ -216,12 +216,13 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement { { type: "empty-state", icon: "mdi:check-all", + icon_color: "primary", content_only: true, title: hass.localize( - "ui.panel.lovelace.strategy.other_devices.empty_state_title" + "ui.panel.lovelace.strategy.home-other-devices.all_organized_title" ), content: hass.localize( - "ui.panel.lovelace.strategy.other_devices.empty_state_content" + "ui.panel.lovelace.strategy.home-other-devices.all_organized_content" ), } as EmptyStateCardConfig, ], diff --git a/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts b/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts index 92db96e5ad..3503c4f451 100644 --- a/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts @@ -20,6 +20,7 @@ import type { HomeAssistant } from "../../../../types"; import type { AreaCardConfig, DiscoveredDevicesCardConfig, + EmptyStateCardConfig, HomeSummaryCard, MarkdownCardConfig, TileCardConfig, @@ -32,6 +33,7 @@ import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters"; export interface HomeOverviewViewStrategyConfig { type: "home-overview"; favorite_entities?: string[]; + home_panel?: boolean; } const computeAreaCard = ( @@ -341,6 +343,59 @@ export class HomeOverviewViewStrategy extends ReactiveElement { } : undefined; + // No sections, show empty state + if (floorsSections.length === 0) { + return { + type: "panel", + cards: [ + { + type: "empty-state", + icon: "mdi:home-assistant", + icon_color: "primary", + content_only: true, + title: hass.localize( + "ui.panel.lovelace.strategy.home.welcome_title" + ), + content: hass.localize( + "ui.panel.lovelace.strategy.home.welcome_content" + ), + ...(config.home_panel && hass.user?.is_admin + ? { + buttons: [ + { + icon: "mdi:plus", + text: hass.localize( + "ui.panel.lovelace.strategy.home.welcome_add_device" + ), + appearance: "filled", + variant: "brand", + tap_action: { + action: "fire-dom-event", + home_panel: { + type: "add_integration", + }, + }, + }, + { + icon: "mdi:home-edit", + text: hass.localize( + "ui.panel.lovelace.strategy.home.welcome_edit_areas" + ), + appearance: "plain", + variant: "brand", + tap_action: { + action: "navigate", + navigation_path: "/config/areas/dashboard", + }, + }, + ], + } + : {}), + } as EmptyStateCardConfig, + ], + }; + } + const sections = ( [favoritesSection, mobileSummarySection, ...floorsSections] satisfies ( | LovelaceSectionRawConfig diff --git a/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts b/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts index c29e8be4ec..b79c38f7b1 100644 --- a/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts +++ b/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts @@ -71,6 +71,7 @@ export class OriginalStatesViewStrategy extends ReactiveElement { { type: "empty-state", icon: "mdi:home-assistant", + icon_color: "primary", content_only: true, title: hass.localize( "ui.panel.lovelace.strategy.original-states.empty_state_title" @@ -80,13 +81,19 @@ export class OriginalStatesViewStrategy extends ReactiveElement { ), ...(hass.user?.is_admin ? { - action_button_text: hass.localize( - "ui.panel.lovelace.strategy.original-states.empty_state_action" - ), - tap_action: { - action: "navigate", - navigation_path: "/config/integrations/dashboard", - }, + buttons: [ + { + text: hass.localize( + "ui.panel.lovelace.strategy.original-states.empty_state_action" + ), + appearance: "filled", + variant: "brand", + tap_action: { + action: "navigate", + navigation_path: "/config/integrations/dashboard", + }, + }, + ], } : {}), } as EmptyStateCardConfig, diff --git a/src/translations/en.json b/src/translations/en.json index 9b67bb48fa..3e5f77b146 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -7524,8 +7524,6 @@ "empty_state_action": "Go to the integrations page" }, "areas": { - "empty_state_title": "No devices", - "empty_state_content": "There are no devices assigned to this area yet. Assign devices to this area to see them here.", "sensors": "Sensors", "sensors_description": "To display temperature and humidity sensors in the overview and in the area view, add a sensor to that area and {edit_the_area} to configure related sensors.", "edit_the_area": "edit the area", @@ -7559,7 +7557,11 @@ "automations": "Automations", "for_you": "For you", "home": "Home", - "favorites": "Favorites" + "favorites": "Favorites", + "welcome_title": "No devices here yet", + "welcome_content": "Add lights, switches, sensors, or other smart home devices to get started.", + "welcome_add_device": "Add new device", + "welcome_edit_areas": "Edit areas" }, "common_controls": { "not_loaded": "Usage Prediction integration is not loaded.", @@ -7581,12 +7583,18 @@ "media_players": "Media players", "other_media_players": "Other media players" }, - "other_devices": { + "home-other-devices": { "helpers": "Helpers", "entities": "Entities", "assign_area": "Assign area", - "empty_state_title": "All devices are organized", - "empty_state_content": "There are no unassigned devices left. All devices are organized into areas." + "all_organized_title": "All devices are organized", + "all_organized_content": "There are no unassigned devices left. All devices are organized into areas." + }, + "home-area": { + "no_devices_title": "This is a blank canvas", + "no_devices_content": "Add your smart lights, switches, or sensors to this area to get started.", + "no_devices_add_device": "Add new device", + "no_devices_assign_device": "Assign existing device" } }, "cards": { @@ -8283,14 +8291,14 @@ }, "empty_state": { "name": "Empty state", - "description": "The Empty state card displays a centered message with an optional icon and action button.", + "description": "The Empty state card displays a centered message with an optional icon and action buttons.", "style": "Style", "style_options": { "card": "Card", "content-only": "Content only" }, "content": "Content", - "action_text": "Action button text" + "buttons": "Buttons" }, "button": { "name": "Button",