1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-02-15 07:25:54 +00:00

Add empty state to Home panel strategies (#29113)

This commit is contained in:
Paul Bottein
2026-01-22 13:15:27 +01:00
committed by GitHub
parent 3231d46835
commit 55c74d7959
10 changed files with 283 additions and 56 deletions

View File

@@ -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) {

View File

@@ -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 {
>
<div class="container">
${this._config.icon
? html`<ha-icon .icon=${this._config.icon}></ha-icon>`
? html`
<ha-icon
class="card-icon"
.icon=${this._config.icon}
style=${styleMap({
color: this._config.icon_color
? computeCssColor(this._config.icon_color)
: undefined,
})}
></ha-icon>
`
: nothing}
${this._config.title ? html`<h1>${this._config.title}</h1>` : nothing}
${this._config.content
? html`<p>${this._config.content}</p>`
: nothing}
${this._config.tap_action && this._config.action_button_text
${this._config.buttons?.length
? html`
<ha-button @click=${this._handleAction}>
${this._config.action_button_text}
</ha-button>
<div class="buttons">
${this._config.buttons.map(
(button, index) => html`
<ha-button
.index=${index}
@click=${this._handleButtonAction}
appearance=${ifDefined(button.appearance)}
variant=${ifDefined(button.variant)}
>
${button.icon
? html`<ha-icon
slot="start"
.icon=${button.icon}
></ha-icon>`
: nothing}
${button.text}
</ha-button>
`
)}
</div>
`
: nothing}
</div>
@@ -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;

View File

@@ -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 {

View File

@@ -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}`
);

View File

@@ -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,
],
};

View File

@@ -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,

View File

@@ -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,
],

View File

@@ -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

View File

@@ -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,

View File

@@ -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",