diff --git a/src/panels/lovelace/cards/hui-empty-state-card.ts b/src/panels/lovelace/cards/hui-empty-state-card.ts index 9969767be2..c38df4e567 100644 --- a/src/panels/lovelace/cards/hui-empty-state-card.ts +++ b/src/panels/lovelace/cards/hui-empty-state-card.ts @@ -1,63 +1,116 @@ import { css, html, LitElement, nothing } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import "../../../components/ha-card"; import "../../../components/ha-button"; +import "../../../components/ha-icon"; import type { HomeAssistant } from "../../../types"; -import type { LovelaceCard } from "../types"; +import { handleAction } from "../common/handle-action"; +import type { LovelaceCard, LovelaceCardEditor } from "../types"; import type { EmptyStateCardConfig } from "./types"; @customElement("hui-empty-state-card") export class HuiEmptyStateCard extends LitElement implements LovelaceCard { + public static async getConfigElement(): Promise { + await import("../editor/config-elements/hui-empty-state-card-editor"); + return document.createElement("hui-empty-state-card-editor"); + } + + public static getStubConfig(): EmptyStateCardConfig { + return { + type: "empty-state", + title: "Welcome Home", + content: "This is an empty state card.", + }; + } + @property({ attribute: false }) public hass?: HomeAssistant; + @state() private _config?: EmptyStateCardConfig; + public getCardSize(): number { return 2; } - // eslint-disable-next-line @typescript-eslint/no-empty-function - public setConfig(_config: EmptyStateCardConfig): void {} + public setConfig(config: EmptyStateCardConfig): void { + this._config = config; + } protected render() { - if (!this.hass) { + if (!this.hass || !this._config) { return nothing; } return html` -
- ${this.hass.localize( - "ui.panel.lovelace.cards.empty_state.no_devices" - )} -
-
- - ${this.hass.localize( - "ui.panel.lovelace.cards.empty_state.go_to_integrations_page" - )} - +
+ ${this._config.icon + ? 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_label + ? html` + + ${this._config.action_label} + + ` + : nothing}
`; } + private _handleAction(): void { + if (this._config?.tap_action && this.hass) { + handleAction(this, this.hass, this._config, "tap"); + } + } + static styles = css` - .content { - margin-top: -1em; - padding: 16px; + :host { + display: block; + height: 100%; } - - .card-actions a { - text-decoration: none; + ha-card { + height: 100%; } - - ha-button { - margin-left: -8px; - margin-inline-start: -8px; - margin-inline-end: initial; + .container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + height: 100%; + padding: var(--ha-space-8) var(--ha-space-4); + box-sizing: border-box; + gap: var(--ha-space-4); + max-width: 640px; + margin: 0 auto; + } + ha-icon { + --mdc-icon-size: var(--ha-space-12); + color: var(--secondary-text-color); + } + h1 { + margin: 0; + font-size: var(--ha-font-size-xl); + font-weight: 500; + } + p { + margin: 0; + color: var(--secondary-text-color); + } + .content-only { + background: none; + box-shadow: none; + border: none; } `; } diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index d7d013073e..0c6697d1b5 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -58,8 +58,12 @@ export interface ConditionalCardConfig extends LovelaceCardConfig { } export interface EmptyStateCardConfig extends LovelaceCardConfig { - content: string; + content_only?: boolean; + icon?: string; title?: string; + content?: string; + action_label?: string; + tap_action?: ActionConfig; } 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 new file mode 100644 index 0000000000..a78a0e592e --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-empty-state-card-editor.ts @@ -0,0 +1,153 @@ +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 { fireEvent } from "../../../../common/dom/fire_event"; +import type { LocalizeFunc } from "../../../../common/translations/localize"; +import "../../../../components/ha-form/ha-form"; +import type { + HaFormSchema, + SchemaUnion, +} from "../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../types"; +import type { EmptyStateCardConfig } from "../../cards/types"; +import type { LovelaceCardEditor } from "../../types"; +import { actionConfigStruct } from "../structs/action-struct"; +import { baseLovelaceCardConfig } from "../structs/base-card-struct"; + +const cardConfigStruct = assign( + baseLovelaceCardConfig, + object({ + content_only: optional(boolean()), + icon: optional(string()), + title: optional(string()), + content: optional(string()), + action_label: optional(string()), + tap_action: optional(actionConfigStruct), + }) +); + +@customElement("hui-empty-state-card-editor") +export class HuiEmptyStateCardEditor + extends LitElement + implements LovelaceCardEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: EmptyStateCardConfig; + + public setConfig(config: EmptyStateCardConfig): void { + assert(config, cardConfigStruct); + this._config = config; + } + + private _schema = memoizeOne( + (localize: LocalizeFunc) => + [ + { + name: "style", + selector: { + select: { + mode: "box", + options: ( + [ + { value: "card", image: "card" }, + { value: "content-only", image: "text_only" }, + ] as const + ).map((style) => ({ + label: localize( + `ui.panel.lovelace.editor.card.empty_state.style_options.${style.value}` + ), + image: { + src: `/static/images/form/markdown_${style.image}.svg`, + src_dark: `/static/images/form/markdown_${style.image}_dark.svg`, + flip_rtl: true, + }, + value: style.value, + })), + }, + }, + }, + { name: "icon", selector: { icon: {} } }, + { name: "title", selector: { text: {} } }, + { name: "content", selector: { text: { multiline: true } } }, + { + name: "interactions", + type: "expandable", + flatten: true, + iconPath: mdiGestureTap, + schema: [ + { name: "action_label", selector: { text: {} } }, + { + name: "tap_action", + selector: { + ui_action: { + default_action: "none", + }, + }, + }, + ], + }, + ] as const satisfies readonly HaFormSchema[] + ); + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + const data = { + ...this._config, + style: this._config.content_only ? "content-only" : "card", + }; + + const schema = this._schema(this.hass.localize); + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + const config = { ...ev.detail.value }; + + if (config.style === "content-only") { + config.content_only = true; + } else { + delete config.content_only; + } + delete config.style; + + fireEvent(this, "config-changed", { config }); + } + + private _computeLabelCallback = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "style": + case "content": + case "action_label": + return this.hass!.localize( + `ui.panel.lovelace.editor.card.empty_state.${schema.name}` + ); + default: + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + } + }; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-empty-state-card-editor": HuiEmptyStateCardEditor; + } +} 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 af9dc1937b..78b0a579f1 100644 --- a/src/panels/lovelace/strategies/home/home-area-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-area-view-strategy.ts @@ -11,7 +11,10 @@ import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section"; import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../../types"; -import type { HeadingCardConfig } from "../../cards/types"; +import type { + EmptyStateCardConfig, + HeadingCardConfig, +} from "../../cards/types"; import { computeAreaTileCardConfig } from "../areas/helpers/areas-strategy-helper"; import { getSummaryLabel, @@ -354,6 +357,26 @@ export class HomeAreaViewStrategy extends ReactiveElement { }); } + // No sections, show empty state + if (sections.length === 0) { + return { + type: "panel", + cards: [ + { + type: "empty-state", + icon: "mdi:sofa-outline", + content_only: true, + title: hass.localize( + "ui.panel.lovelace.strategy.areas.empty_state_title" + ), + content: hass.localize( + "ui.panel.lovelace.strategy.areas.empty_state_content" + ), + } as EmptyStateCardConfig, + ], + }; + } + // Allow between 2 and 3 columns (the max should be set to define the width of the header) const maxColumns = clamp(sections.length, 2, 3); 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 fd6c1f65ae..aeb56eec48 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 @@ -6,6 +6,7 @@ import type { AreasDisplayValue } from "../../../../components/ha-areas-display- import { getEnergyPreferences } from "../../../../data/energy"; import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../../types"; +import type { EmptyStateCardConfig } from "../../cards/types"; import { generateDefaultViewConfig } from "../../common/generate-lovelace-config"; export interface OriginalStatesViewStrategyConfig { @@ -64,9 +65,33 @@ export class OriginalStatesViewStrategy extends ReactiveElement { // User has no entities if (view.cards!.length === 0) { - view.cards!.push({ - type: "empty-state", - }); + return { + type: "panel", + cards: [ + { + type: "empty-state", + icon: "mdi:home-assistant", + content_only: true, + title: hass.localize( + "ui.panel.lovelace.strategy.original-states.empty_state_title" + ), + content: hass.localize( + "ui.panel.lovelace.strategy.original-states.empty_state_content" + ), + ...(hass.user?.is_admin + ? { + action_label: hass.localize( + "ui.panel.lovelace.strategy.original-states.empty_state_action" + ), + tap_action: { + action: "navigate", + navigation_path: "/config/integrations/dashboard", + }, + } + : {}), + } as EmptyStateCardConfig, + ], + }; } return view; diff --git a/src/translations/en.json b/src/translations/en.json index cd6c27fa80..cb5782fd88 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -7233,10 +7233,14 @@ "lovelace": { "strategy": { "original-states": { - "helpers": "[%key:ui::panel::config::helpers::caption%]" + "helpers": "[%key:ui::panel::config::helpers::caption%]", + "empty_state_title": "Welcome Home", + "empty_state_content": "This page allows you to control your devices, however it looks like you have no devices set up yet.", + "empty_state_action": "Go to the integrations page" }, "areas": { - "no_entities": "No entities in this area.", + "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", @@ -7308,11 +7312,6 @@ "no_url": "No URL to open specified", "no_action": "No action to run specified" }, - "empty_state": { - "title": "Welcome Home", - "no_devices": "This page allows you to control your devices, however it looks like you have no devices set up yet. Head to the integrations page to get started.", - "go_to_integrations_page": "Go to the integrations page." - }, "entities": { "never_triggered": "Never triggered" }, @@ -7984,6 +7983,17 @@ "name": "Entity", "description": "The Entity card gives you a quick overview of your entity's state." }, + "empty_state": { + "name": "Empty state", + "description": "The Empty state card displays a centered message with an optional icon and action button.", + "style": "Style", + "style_options": { + "card": "Card", + "content-only": "Content only" + }, + "content": "Content", + "action_label": "Action label" + }, "button": { "name": "Button", "description": "The Button card allows you to add buttons to perform tasks.",