Always show compact header for dashboards (#26706)

This commit is contained in:
Paul Bottein
2025-08-26 20:09:57 +02:00
committed by GitHub
parent 51840b88b3
commit 317149e51e
4 changed files with 217 additions and 55 deletions

View File

@@ -148,6 +148,10 @@ export class HaDialog extends DialogBase {
white-space: nowrap; white-space: nowrap;
display: block; display: block;
padding-left: 4px; padding-left: 4px;
padding-right: 4px;
margin-right: 12px;
margin-inline-end: 12px;
margin-inline-start: initial;
} }
.header_button { .header_button {
text-decoration: none; text-decoration: none;

View File

@@ -0,0 +1,106 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-icon";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/ha-svg-icon";
import type { HomeAssistant } from "../../types";
import type { HassDialog } from "../make-dialog-manager";
import type { ListItemsDialogParams } from "./show-list-items-dialog";
@customElement("dialog-list-items")
export class ListItemsDialog
extends LitElement
implements HassDialog<ListItemsDialogParams>
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _params?: ListItemsDialogParams;
public async showDialog(params: ListItemsDialogParams): Promise<void> {
this._params = params;
}
private _dialogClosed(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _itemClicked(ev: CustomEvent): void {
const item = (ev.currentTarget as any).item;
if (!item) return;
item.action();
this._dialogClosed();
}
protected render() {
if (!this._params || !this.hass) {
return nothing;
}
return html`
<ha-dialog
open
.heading=${createCloseHeading(this.hass, this._params.title ?? " ")}
@closed=${this._dialogClosed}
hideActions
>
<div class="container">
<ha-md-list>
${this._params.items.map(
(item) => html`
<ha-md-list-item
type="button"
@click=${this._itemClicked}
.item=${item}
>
${item.iconPath
? html`
<ha-svg-icon
.path=${item.iconPath}
slot="start"
class="item-icon"
></ha-svg-icon>
`
: item.icon
? html`
<ha-icon
icon=${item.icon}
slot="start"
class="item-icon"
></ha-icon>
`
: nothing}
<span class="headline">${item.label}</span>
${item.description
? html`
<span class="supporting-text">${item.description}</span>
`
: nothing}
</ha-md-list-item>
`
)}
</ha-md-list>
</div>
</ha-dialog>
`;
}
static styles = css`
ha-dialog {
/* Place above other dialogs */
--dialog-z-index: 104;
--dialog-content-padding: 0;
--md-list-item-leading-space: 24px;
--md-list-item-trailing-space: 24px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"dialog-list-items": ListItemsDialog;
}
}

View File

@@ -0,0 +1,24 @@
import { fireEvent } from "../../common/dom/fire_event";
interface ListItem {
icon?: string;
iconPath?: string;
label: string;
description?: string;
action: () => any;
}
export interface ListItemsDialogParams {
title?: string;
items: ListItem[];
}
export const showListItemsDialog = (
element: HTMLElement,
params: ListItemsDialogParams
) =>
fireEvent(element, "show-dialog", {
dialogTag: "dialog-list-items",
dialogImport: () => import("./dialog-list-items"),
dialogParams: params,
});

View File

@@ -46,6 +46,7 @@ import "../../components/ha-list-item";
import "../../components/ha-menu-button"; import "../../components/ha-menu-button";
import "../../components/ha-svg-icon"; import "../../components/ha-svg-icon";
import "../../components/sl-tab-group"; import "../../components/sl-tab-group";
import { createAreaRegistryEntry } from "../../data/area_registry";
import type { LovelacePanelConfig } from "../../data/lovelace"; import type { LovelacePanelConfig } from "../../data/lovelace";
import type { LovelaceConfig } from "../../data/lovelace/config/types"; import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { isStrategyDashboard } from "../../data/lovelace/config/types"; import { isStrategyDashboard } from "../../data/lovelace/config/types";
@@ -57,6 +58,7 @@ import {
} from "../../data/lovelace/dashboard"; } from "../../data/lovelace/dashboard";
import { getPanelTitle } from "../../data/panel"; import { getPanelTitle } from "../../data/panel";
import { createPerson } from "../../data/person"; import { createPerson } from "../../data/person";
import { showListItemsDialog } from "../../dialogs/dialog-list-items/show-list-items-dialog";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@@ -71,6 +73,8 @@ import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import type { HomeAssistant, PanelInfo } from "../../types"; import type { HomeAssistant, PanelInfo } from "../../types";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
import { showToast } from "../../util/toast";
import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail";
import { showNewAutomationDialog } from "../config/automation/show-dialog-new-automation"; import { showNewAutomationDialog } from "../config/automation/show-dialog-new-automation";
import { showAddIntegrationDialog } from "../config/integrations/show-add-integration-dialog"; import { showAddIntegrationDialog } from "../config/integrations/show-add-integration-dialog";
import { showDashboardDetailDialog } from "../config/lovelace/dashboards/show-dialog-lovelace-dashboard-detail"; import { showDashboardDetailDialog } from "../config/lovelace/dashboards/show-dialog-lovelace-dashboard-detail";
@@ -86,9 +90,6 @@ import "./views/hui-view";
import type { HUIView } from "./views/hui-view"; import type { HUIView } from "./views/hui-view";
import "./views/hui-view-background"; import "./views/hui-view-background";
import "./views/hui-view-container"; import "./views/hui-view-container";
import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail";
import { createAreaRegistryEntry } from "../../data/area_registry";
import { showToast } from "../../util/toast";
interface ActionItem { interface ActionItem {
icon: string; icon: string;
@@ -105,6 +106,7 @@ interface ActionItem {
interface SubActionItem { interface SubActionItem {
icon: string; icon: string;
key: LocalizeKeys; key: LocalizeKeys;
overflowAction?: any;
action?: any; action?: any;
visible: boolean | undefined; visible: boolean | undefined;
} }
@@ -208,31 +210,35 @@ class HUIRoot extends LitElement {
icon: mdiPlus, icon: mdiPlus,
key: "ui.panel.lovelace.menu.add", key: "ui.panel.lovelace.menu.add",
visible: !this._editMode && this.hass.user?.is_admin, visible: !this._editMode && this.hass.user?.is_admin,
overflow: false, overflow: this.narrow,
subItems: [ subItems: [
{ {
icon: mdiDevices, icon: mdiDevices,
key: "ui.panel.lovelace.menu.add_device", key: "ui.panel.lovelace.menu.add_device",
visible: true, visible: true,
action: this._handleAddDevice, action: this._addDevice,
overflowAction: this._handleAddDevice,
}, },
{ {
icon: mdiRobot, icon: mdiRobot,
key: "ui.panel.lovelace.menu.create_automation", key: "ui.panel.lovelace.menu.create_automation",
visible: true, visible: true,
action: this._handleACreateAutomation, action: this._createAutomation,
overflowAction: this._handleCreateAutomation,
}, },
{ {
icon: mdiSofa, icon: mdiSofa,
key: "ui.panel.lovelace.menu.add_area", key: "ui.panel.lovelace.menu.add_area",
visible: true, visible: true,
action: this._handleAddArea, action: this._addArea,
overflowAction: this._handleAddArea,
}, },
{ {
icon: mdiAccount, icon: mdiAccount,
key: "ui.panel.lovelace.menu.add_person", key: "ui.panel.lovelace.menu.add_person",
visible: true, visible: true,
action: this._handleInvitePerson, action: this._addPerson,
overflowAction: this._handleAddPerson,
}, },
], ],
}, },
@@ -242,7 +248,7 @@ class HUIRoot extends LitElement {
buttonAction: this._showQuickBar, buttonAction: this._showQuickBar,
overflowAction: this._handleShowQuickBar, overflowAction: this._handleShowQuickBar,
visible: !this._editMode, visible: !this._editMode,
overflow: false, overflow: this.narrow,
suffix: this.hass.enableShortcuts ? "(E)" : undefined, suffix: this.hass.enableShortcuts ? "(E)" : undefined,
}, },
{ {
@@ -252,7 +258,7 @@ class HUIRoot extends LitElement {
overflowAction: this._handleShowVoiceCommandDialog, overflowAction: this._handleShowVoiceCommandDialog,
visible: visible:
!this._editMode && this._conversation(this.hass.config.components), !this._editMode && this._conversation(this.hass.config.components),
overflow: false, overflow: this.narrow,
suffix: this.hass.enableShortcuts ? "(A)" : undefined, suffix: this.hass.enableShortcuts ? "(A)" : undefined,
}, },
{ {
@@ -321,7 +327,7 @@ class HUIRoot extends LitElement {
<ha-list-item <ha-list-item
graphic="icon" graphic="icon"
.key=${subItem.key} .key=${subItem.key}
@request-selected=${subItem.action} @request-selected=${subItem.overflowAction}
> >
${this.hass!.localize(subItem.key)} ${this.hass!.localize(subItem.key)}
<ha-svg-icon <ha-svg-icon
@@ -347,26 +353,37 @@ class HUIRoot extends LitElement {
if (overflowItems.length && !overflowCanPromote) { if (overflowItems.length && !overflowCanPromote) {
const listItems: TemplateResult[] = []; const listItems: TemplateResult[] = [];
overflowItems.forEach((i) => { overflowItems.forEach((i) => {
const title = [this.hass!.localize(i.key), i.suffix].join(" ");
const action = i.subItems
? () => {
showListItemsDialog(this, {
title: title,
items: i.subItems!.map((si) => ({
iconPath: si.icon,
label: this.hass!.localize(si.key),
action: si.action,
})),
});
}
: i.overflowAction;
listItems.push( listItems.push(
html`<ha-list-item html`<ha-list-item graphic="icon" @request-selected=${action}>
graphic="icon" ${title}
@request-selected=${i.overflowAction}
>
${[this.hass!.localize(i.key), i.suffix].join(" ")}
<ha-svg-icon slot="graphic" .path=${i.icon}></ha-svg-icon> <ha-svg-icon slot="graphic" .path=${i.icon}></ha-svg-icon>
</ha-list-item>` </ha-list-item>`
); );
}); });
result.push( result.push(html`
html`<ha-button-menu slot="actionItems"> <ha-button-menu slot="actionItems">
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
.label=${this.hass!.localize("ui.panel.lovelace.editor.menu.open")} .label=${this.hass!.localize("ui.panel.lovelace.editor.menu.open")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
></ha-icon-button> ></ha-icon-button>
${listItems} ${listItems}
</ha-button-menu>` </ha-button-menu>
); `);
} }
return html`${result}`; return html`${result}`;
} }
@@ -458,8 +475,6 @@ class HUIRoot extends LitElement {
const isSubview = curViewConfig?.subview; const isSubview = curViewConfig?.subview;
const hasTabViews = views.filter((view) => !view.subview).length > 1; const hasTabViews = views.filter((view) => !view.subview).length > 1;
const showTabBar =
this._editMode || (!isSubview && hasTabViews && this.narrow);
return html` return html`
<div <div
@@ -504,35 +519,35 @@ class HUIRoot extends LitElement {
`} `}
${isSubview ${isSubview
? html`<div class="main-title">${curViewConfig.title}</div>` ? html`<div class="main-title">${curViewConfig.title}</div>`
: hasTabViews && !showTabBar : hasTabViews
? tabs ? tabs
: html` : html`
<div class="main-title"> <div class="main-title">
${curViewConfig?.title ?? dashboardTitle} ${views[0]?.title ?? dashboardTitle}
</div> </div>
`} `}
<div class="action-items">${this._renderActionItems()}</div> <div class="action-items">${this._renderActionItems()}</div>
`} `}
</div> </div>
${showTabBar ${this._editMode
? html`<div class="tab-bar"> ? html`
${tabs} <div class="tab-bar">
${this._editMode ${tabs}
? html`<ha-icon-button <ha-icon-button
slot="nav" slot="nav"
id="add-view" id="add-view"
@click=${this._addView} @click=${this._addView}
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.add" "ui.panel.lovelace.editor.edit_view.add"
)} )}
.path=${mdiPlus} .path=${mdiPlus}
></ha-icon-button>` ></ha-icon-button>
: nothing} </div>
</div>` `
: nothing} : nothing}
</div> </div>
<hui-view-container <hui-view-container
class=${showTabBar ? "has-tab-bar" : ""} class=${this._editMode ? "has-tab-bar" : ""}
.hass=${this.hass} .hass=${this.hass}
.theme=${curViewConfig?.theme} .theme=${curViewConfig?.theme}
id="view" id="view"
@@ -788,36 +803,47 @@ class HUIRoot extends LitElement {
} }
} }
private async _handleAddDevice( private _handleAddDevice(ev: CustomEvent<RequestSelectedDetail>): void {
ev: CustomEvent<RequestSelectedDetail>
): Promise<void> {
if (!shouldHandleRequestSelectedEvent(ev)) { if (!shouldHandleRequestSelectedEvent(ev)) {
return; return;
} }
this._addDevice();
}
private _addDevice = async () => {
await this.hass.loadFragmentTranslation("config"); await this.hass.loadFragmentTranslation("config");
showAddIntegrationDialog(this); showAddIntegrationDialog(this);
} };
private async _handleACreateAutomation( private _handleCreateAutomation(
ev: CustomEvent<RequestSelectedDetail> ev: CustomEvent<RequestSelectedDetail>
): Promise<void> { ): void {
if (!shouldHandleRequestSelectedEvent(ev)) { if (!shouldHandleRequestSelectedEvent(ev)) {
return; return;
} }
this._createAutomation();
}
private _createAutomation = async () => {
await this.hass.loadFragmentTranslation("config"); await this.hass.loadFragmentTranslation("config");
showNewAutomationDialog(this, { mode: "automation" }); showNewAutomationDialog(this, { mode: "automation" });
} };
private async _handleAddArea( private _handleAddArea(ev: CustomEvent<RequestSelectedDetail>): void {
ev: CustomEvent<RequestSelectedDetail>
): Promise<void> {
if (!shouldHandleRequestSelectedEvent(ev)) { if (!shouldHandleRequestSelectedEvent(ev)) {
return; return;
} }
this._addArea();
}
private _addArea = async () => {
await this.hass.loadFragmentTranslation("config"); await this.hass.loadFragmentTranslation("config");
showAreaRegistryDetailDialog(this, { showAreaRegistryDetailDialog(this, {
createEntry: async (values) => { createEntry: async (values) => {
const area = await createAreaRegistryEntry(this.hass, values); const area = await createAreaRegistryEntry(this.hass, values);
if (isStrategyDashboard(this.lovelace!.rawConfig)) {
fireEvent(this, "config-refresh");
}
showToast(this, { showToast(this, {
message: this.hass.localize( message: this.hass.localize(
"ui.panel.lovelace.menu.add_area_success" "ui.panel.lovelace.menu.add_area_success"
@@ -831,14 +857,16 @@ class HUIRoot extends LitElement {
}); });
}, },
}); });
} };
private async _handleInvitePerson( private _handleAddPerson(ev: CustomEvent<RequestSelectedDetail>): void {
ev: CustomEvent<RequestSelectedDetail>
): Promise<void> {
if (!shouldHandleRequestSelectedEvent(ev)) { if (!shouldHandleRequestSelectedEvent(ev)) {
return; return;
} }
this._addPerson();
}
private _addPerson = async () => {
await this.hass.loadFragmentTranslation("config"); await this.hass.loadFragmentTranslation("config");
showPersonDetailDialog(this, { showPersonDetailDialog(this, {
users: [], users: [],
@@ -859,7 +887,7 @@ class HUIRoot extends LitElement {
}); });
}, },
}); });
} };
private _handleRawEditor(ev: CustomEvent<RequestSelectedDetail>): void { private _handleRawEditor(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) { if (!shouldHandleRequestSelectedEvent(ev)) {