mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 00:27:49 +01:00
Allow customizing home page summaries and adding quick links
Add ability to hide built-in summaries (light, climate, security, media players, weather, energy) and add custom quick links to dashboards, sidebar items, or other pages from the edit overview dialog. https://claude.ai/code/session_01AqgbQULH5vfETibiba5RXH
This commit is contained in:
@@ -17,9 +17,17 @@ export interface CoreFrontendSystemData {
|
||||
onboarded_date?: string;
|
||||
}
|
||||
|
||||
export interface HomeQuickLink {
|
||||
name: string;
|
||||
icon?: string;
|
||||
navigation_path: string;
|
||||
}
|
||||
|
||||
export interface HomeFrontendSystemData {
|
||||
favorite_entities?: string[];
|
||||
welcome_banner_dismissed?: boolean;
|
||||
hidden_summaries?: string[];
|
||||
quick_links?: HomeQuickLink[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
import { mdiClose, mdiPlus } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/entity/ha-entities-picker";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-dialog";
|
||||
import type { HomeFrontendSystemData } from "../../../data/frontend";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-navigation-picker";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import type {
|
||||
HomeFrontendSystemData,
|
||||
HomeQuickLink,
|
||||
} from "../../../data/frontend";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import {
|
||||
HOME_SUMMARIES,
|
||||
getSummaryLabel,
|
||||
type HomeSummary,
|
||||
} from "../../../panels/lovelace/strategies/home/helpers/home-summaries";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { EditHomeDialogParams } from "./show-dialog-edit-home";
|
||||
|
||||
const ALL_SUMMARY_KEYS = [...HOME_SUMMARIES, "weather"] as const;
|
||||
|
||||
@customElement("dialog-edit-home")
|
||||
export class DialogEditHome
|
||||
extends LitElement
|
||||
@@ -50,6 +68,9 @@ export class DialogEditHome
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const hiddenSummaries = new Set(this._config?.hidden_summaries || []);
|
||||
const quickLinks = this._config?.quick_links || [];
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
@@ -79,6 +100,88 @@ export class DialogEditHome
|
||||
@value-changed=${this._favoriteEntitiesChanged}
|
||||
></ha-entities-picker>
|
||||
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize("ui.panel.home.editor.summaries")}
|
||||
</h3>
|
||||
<p class="section-description">
|
||||
${this.hass.localize("ui.panel.home.editor.summaries_description")}
|
||||
</p>
|
||||
<div class="summary-toggles">
|
||||
${ALL_SUMMARY_KEYS.map((key) => {
|
||||
const label = this._getSummaryLabel(key);
|
||||
return html`
|
||||
<label class="summary-toggle">
|
||||
<ha-switch
|
||||
.checked=${!hiddenSummaries.has(key)}
|
||||
.summary=${key}
|
||||
@change=${this._summaryToggleChanged}
|
||||
></ha-switch>
|
||||
<span>${label}</span>
|
||||
</label>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize("ui.panel.home.editor.quick_links")}
|
||||
</h3>
|
||||
<p class="section-description">
|
||||
${this.hass.localize("ui.panel.home.editor.quick_links_description")}
|
||||
</p>
|
||||
<div class="quick-links">
|
||||
${repeat(
|
||||
quickLinks,
|
||||
(_link, index) => index,
|
||||
(link, index) => html`
|
||||
<div class="quick-link-row">
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
.value=${link.icon || ""}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.home.editor.quick_link_icon"
|
||||
)}
|
||||
@value-changed=${(ev: CustomEvent) =>
|
||||
this._quickLinkChanged(index, "icon", ev.detail.value)}
|
||||
></ha-icon-picker>
|
||||
<ha-textfield
|
||||
.value=${link.name}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.home.editor.quick_link_name"
|
||||
)}
|
||||
@input=${(ev: InputEvent) =>
|
||||
this._quickLinkChanged(
|
||||
index,
|
||||
"name",
|
||||
(ev.target as HTMLInputElement).value
|
||||
)}
|
||||
></ha-textfield>
|
||||
<ha-navigation-picker
|
||||
.hass=${this.hass}
|
||||
.value=${link.navigation_path}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.home.editor.quick_link_path"
|
||||
)}
|
||||
@value-changed=${(ev: CustomEvent) =>
|
||||
this._quickLinkChanged(
|
||||
index,
|
||||
"navigation_path",
|
||||
ev.detail.value
|
||||
)}
|
||||
></ha-navigation-picker>
|
||||
<ha-icon-button
|
||||
.path=${mdiClose}
|
||||
.label=${this.hass.localize("ui.common.remove")}
|
||||
@click=${() => this._removeQuickLink(index)}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<ha-button @click=${this._addQuickLink}>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.home.editor.add_quick_link")}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize("ui.panel.home.editor.areas_hint", {
|
||||
areas_page: html`<a
|
||||
@@ -110,6 +213,69 @@ export class DialogEditHome
|
||||
`;
|
||||
}
|
||||
|
||||
private _getSummaryLabel(key: string): string {
|
||||
if (key === "weather") {
|
||||
return this.hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.summary_list.weather"
|
||||
);
|
||||
}
|
||||
return getSummaryLabel(this.hass.localize, key as HomeSummary);
|
||||
}
|
||||
|
||||
private _summaryToggleChanged(ev: Event): void {
|
||||
const target = ev.target as HTMLElement & {
|
||||
checked: boolean;
|
||||
summary: string;
|
||||
};
|
||||
const summary = target.summary;
|
||||
const checked = target.checked;
|
||||
|
||||
const hiddenSummaries = new Set(this._config?.hidden_summaries || []);
|
||||
|
||||
if (checked) {
|
||||
hiddenSummaries.delete(summary);
|
||||
} else {
|
||||
hiddenSummaries.add(summary);
|
||||
}
|
||||
|
||||
this._config = {
|
||||
...this._config,
|
||||
hidden_summaries:
|
||||
hiddenSummaries.size > 0 ? [...hiddenSummaries] : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private _quickLinkChanged(
|
||||
index: number,
|
||||
field: keyof HomeQuickLink,
|
||||
value: string
|
||||
): void {
|
||||
const quickLinks = [...(this._config?.quick_links || [])];
|
||||
quickLinks[index] = { ...quickLinks[index], [field]: value };
|
||||
this._config = {
|
||||
...this._config,
|
||||
quick_links: quickLinks,
|
||||
};
|
||||
}
|
||||
|
||||
private _removeQuickLink(index: number): void {
|
||||
const quickLinks = [...(this._config?.quick_links || [])];
|
||||
quickLinks.splice(index, 1);
|
||||
this._config = {
|
||||
...this._config,
|
||||
quick_links: quickLinks.length > 0 ? quickLinks : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private _addQuickLink(): void {
|
||||
const quickLinks = [...(this._config?.quick_links || [])];
|
||||
quickLinks.push({ name: "", icon: "mdi:link", navigation_path: "" });
|
||||
this._config = {
|
||||
...this._config,
|
||||
quick_links: quickLinks,
|
||||
};
|
||||
}
|
||||
|
||||
private _favoriteEntitiesChanged(ev: CustomEvent): void {
|
||||
const entities = ev.detail.value as string[];
|
||||
this._config = {
|
||||
@@ -148,6 +314,63 @@ export class DialogEditHome
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: var(--ha-space-6) 0 var(--ha-space-1) 0;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
margin: 0 0 var(--ha-space-2) 0;
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.summary-toggles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.summary-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-3);
|
||||
padding: var(--ha-space-1) 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quick-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-3);
|
||||
}
|
||||
|
||||
.quick-link-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.quick-link-row ha-icon-picker {
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quick-link-row ha-textfield {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.quick-link-row ha-navigation-picker {
|
||||
flex: 2;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.quick-link-row ha-icon-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
ha-entities-picker {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -318,6 +318,8 @@ class PanelHome extends LitElement {
|
||||
type: "home",
|
||||
favorite_entities: this._config.favorite_entities,
|
||||
home_panel: true,
|
||||
hidden_summaries: this._config.hidden_summaries,
|
||||
quick_links: this._config.quick_links,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface HomeDashboardStrategyConfig {
|
||||
type: "home";
|
||||
favorite_entities?: string[];
|
||||
home_panel?: boolean;
|
||||
hidden_summaries?: string[];
|
||||
quick_links?: { name: string; icon?: string; navigation_path: string }[];
|
||||
}
|
||||
|
||||
@customElement("home-dashboard-strategy")
|
||||
@@ -94,6 +96,8 @@ export class HomeDashboardStrategy extends ReactiveElement {
|
||||
type: "home-overview",
|
||||
favorite_entities: config.favorite_entities,
|
||||
home_panel: config.home_panel,
|
||||
hidden_summaries: config.hidden_summaries,
|
||||
quick_links: config.quick_links,
|
||||
} satisfies HomeOverviewViewStrategyConfig,
|
||||
},
|
||||
...areaViews,
|
||||
|
||||
@@ -39,6 +39,8 @@ export interface HomeOverviewViewStrategyConfig {
|
||||
type: "home-overview";
|
||||
favorite_entities?: string[];
|
||||
home_panel?: boolean;
|
||||
hidden_summaries?: string[];
|
||||
quick_links?: { name: string; icon?: string; navigation_path: string }[];
|
||||
}
|
||||
|
||||
const computeAreaCard = (
|
||||
@@ -254,6 +256,8 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
) ??
|
||||
false);
|
||||
|
||||
const hiddenSummaries = new Set(config.hidden_summaries || []);
|
||||
|
||||
// Build summary cards (used in both mobile section and sidebar)
|
||||
const summaryCards: LovelaceCardConfig[] = [
|
||||
// Repairs card - only visible to admins, hides when empty
|
||||
@@ -280,6 +284,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
hide_empty: true,
|
||||
} satisfies DiscoveredDevicesCardConfig,
|
||||
hasLights &&
|
||||
!hiddenSummaries.has("light") &&
|
||||
({
|
||||
type: "home-summary",
|
||||
summary: "light",
|
||||
@@ -289,6 +294,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
hasClimate &&
|
||||
!hiddenSummaries.has("climate") &&
|
||||
({
|
||||
type: "home-summary",
|
||||
summary: "climate",
|
||||
@@ -298,6 +304,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
hasSecurity &&
|
||||
!hiddenSummaries.has("security") &&
|
||||
({
|
||||
type: "home-summary",
|
||||
summary: "security",
|
||||
@@ -307,6 +314,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
hasMediaPlayers &&
|
||||
!hiddenSummaries.has("media_players") &&
|
||||
({
|
||||
type: "home-summary",
|
||||
summary: "media_players",
|
||||
@@ -316,6 +324,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
weatherEntity &&
|
||||
!hiddenSummaries.has("weather") &&
|
||||
({
|
||||
type: "tile",
|
||||
entity: weatherEntity,
|
||||
@@ -325,6 +334,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
state_content: ["temperature", "state"],
|
||||
} satisfies TileCardConfig),
|
||||
hasEnergy &&
|
||||
!hiddenSummaries.has("energy") &&
|
||||
({
|
||||
type: "home-summary",
|
||||
summary: "energy",
|
||||
@@ -335,6 +345,21 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
: "/energy?historyBack=1",
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
// Quick links (chip-like cards without summary line)
|
||||
...(config.quick_links || []).map(
|
||||
(link) =>
|
||||
({
|
||||
type: "tile",
|
||||
entity: "zone.home",
|
||||
name: link.name,
|
||||
icon: link.icon || "mdi:link",
|
||||
hide_state: true,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: link.navigation_path,
|
||||
},
|
||||
}) satisfies TileCardConfig
|
||||
),
|
||||
].filter(Boolean) as LovelaceCardConfig[];
|
||||
|
||||
// Build summary cards for sidebar (full width: columns 12)
|
||||
|
||||
@@ -2444,7 +2444,15 @@
|
||||
"favorite_entities_helper": "Display your favorite entities. Home Assistant will still suggest based on commonly used up to 8 slots.",
|
||||
"save_failed": "Failed to save Overview page configuration",
|
||||
"areas_hint": "You can rearrange your floors and areas in the order that best represents your house on the {areas_page}.",
|
||||
"areas_page": "areas page"
|
||||
"areas_page": "areas page",
|
||||
"summaries": "Summaries",
|
||||
"summaries_description": "Choose which summaries to show on your overview page.",
|
||||
"quick_links": "Quick links",
|
||||
"quick_links_description": "Add links to dashboards, sidebar items, or other pages.",
|
||||
"add_quick_link": "Add quick link",
|
||||
"quick_link_name": "Name",
|
||||
"quick_link_icon": "Icon",
|
||||
"quick_link_path": "Navigation path"
|
||||
},
|
||||
"new_overview_dialog": {
|
||||
"title": "Welcome to your new overview",
|
||||
|
||||
Reference in New Issue
Block a user