From 05035a281b12bb9bb2990c2bf1fb1e79f0d80e28 Mon Sep 17 00:00:00 2001 From: quinnter <45169425+quinnter@users.noreply.github.com> Date: Sat, 14 Jun 2025 09:55:14 +0100 Subject: [PATCH] Improved new dashboard dialog (#25676) * tidied cards and more translations * fix container heading padding * updated filter to use fusejs, changes to how list is localized --- .../dark/icon-dashboard-areas.svg | 36 ++ .../dark/icon-dashboard-default.svg | 63 ++++ .../dark/icon-dashboard-map.svg | 16 + .../dark/icon-dashboard-new.svg | 13 + .../dark/icon-dashboard-webpage.svg | 19 + .../light/icon-dashboard-areas.svg | 36 ++ .../light/icon-dashboard-default.svg | 63 ++++ .../light/icon-dashboard-map.svg | 16 + .../light/icon-dashboard-new.svg | 13 + .../light/icon-dashboard-webpage.svg | 16 + src/panels/config/dashboard/dashboard-card.ts | 84 +++++ .../config/dashboard/dialog-new-dashboard.ts | 328 ++++++++++++------ src/translations/en.json | 8 + 13 files changed, 610 insertions(+), 101 deletions(-) create mode 100644 public/static/images/dashboard-options/dark/icon-dashboard-areas.svg create mode 100644 public/static/images/dashboard-options/dark/icon-dashboard-default.svg create mode 100644 public/static/images/dashboard-options/dark/icon-dashboard-map.svg create mode 100644 public/static/images/dashboard-options/dark/icon-dashboard-new.svg create mode 100644 public/static/images/dashboard-options/dark/icon-dashboard-webpage.svg create mode 100644 public/static/images/dashboard-options/light/icon-dashboard-areas.svg create mode 100644 public/static/images/dashboard-options/light/icon-dashboard-default.svg create mode 100644 public/static/images/dashboard-options/light/icon-dashboard-map.svg create mode 100644 public/static/images/dashboard-options/light/icon-dashboard-new.svg create mode 100644 public/static/images/dashboard-options/light/icon-dashboard-webpage.svg create mode 100644 src/panels/config/dashboard/dashboard-card.ts diff --git a/public/static/images/dashboard-options/dark/icon-dashboard-areas.svg b/public/static/images/dashboard-options/dark/icon-dashboard-areas.svg new file mode 100644 index 0000000000..136bf63783 --- /dev/null +++ b/public/static/images/dashboard-options/dark/icon-dashboard-areas.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/static/images/dashboard-options/dark/icon-dashboard-default.svg b/public/static/images/dashboard-options/dark/icon-dashboard-default.svg new file mode 100644 index 0000000000..6106e63db6 --- /dev/null +++ b/public/static/images/dashboard-options/dark/icon-dashboard-default.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/static/images/dashboard-options/dark/icon-dashboard-map.svg b/public/static/images/dashboard-options/dark/icon-dashboard-map.svg new file mode 100644 index 0000000000..e5f4fab10f --- /dev/null +++ b/public/static/images/dashboard-options/dark/icon-dashboard-map.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/static/images/dashboard-options/dark/icon-dashboard-new.svg b/public/static/images/dashboard-options/dark/icon-dashboard-new.svg new file mode 100644 index 0000000000..cf8a49544d --- /dev/null +++ b/public/static/images/dashboard-options/dark/icon-dashboard-new.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/static/images/dashboard-options/dark/icon-dashboard-webpage.svg b/public/static/images/dashboard-options/dark/icon-dashboard-webpage.svg new file mode 100644 index 0000000000..e08ce03ed4 --- /dev/null +++ b/public/static/images/dashboard-options/dark/icon-dashboard-webpage.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/static/images/dashboard-options/light/icon-dashboard-areas.svg b/public/static/images/dashboard-options/light/icon-dashboard-areas.svg new file mode 100644 index 0000000000..3ff70fef26 --- /dev/null +++ b/public/static/images/dashboard-options/light/icon-dashboard-areas.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/static/images/dashboard-options/light/icon-dashboard-default.svg b/public/static/images/dashboard-options/light/icon-dashboard-default.svg new file mode 100644 index 0000000000..498656aa5a --- /dev/null +++ b/public/static/images/dashboard-options/light/icon-dashboard-default.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/static/images/dashboard-options/light/icon-dashboard-map.svg b/public/static/images/dashboard-options/light/icon-dashboard-map.svg new file mode 100644 index 0000000000..3b8fe72669 --- /dev/null +++ b/public/static/images/dashboard-options/light/icon-dashboard-map.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/static/images/dashboard-options/light/icon-dashboard-new.svg b/public/static/images/dashboard-options/light/icon-dashboard-new.svg new file mode 100644 index 0000000000..01ff900ca9 --- /dev/null +++ b/public/static/images/dashboard-options/light/icon-dashboard-new.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/static/images/dashboard-options/light/icon-dashboard-webpage.svg b/public/static/images/dashboard-options/light/icon-dashboard-webpage.svg new file mode 100644 index 0000000000..819dcd918e --- /dev/null +++ b/public/static/images/dashboard-options/light/icon-dashboard-webpage.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/panels/config/dashboard/dashboard-card.ts b/src/panels/config/dashboard/dashboard-card.ts new file mode 100644 index 0000000000..b737152442 --- /dev/null +++ b/src/panels/config/dashboard/dashboard-card.ts @@ -0,0 +1,84 @@ +import { LitElement, html, css } from "lit"; +import { customElement, property } from "lit/decorators"; +import "../../../components/ha-ripple"; + +@customElement("dashboard-card") +export class DashboardCard extends LitElement { + @property({ type: String }) name = ""; + + @property({ type: String }) description = ""; + + @property({ type: String }) img = ""; + + @property({ type: String }) alt = ""; + + render() { + return html` +
+
+
+

${this.name}

+

${this.description}

+
+
+
+ ${this.alt} +
+ +
+ `; + } + + private _onKeyDown(e: KeyboardEvent) { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + this.click(); + } + } + + static styles = css` + .card { + height: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + border-radius: 12px; + background: var(--primary-background-color, #fafafa); + cursor: pointer; + position: relative; + overflow: hidden; + border: 1px solid var(--divider-color); + } + .card-header { + padding: 12px; + display: block; + text-align: left; + gap: 8px; + } + .preview { + padding: 16px; + } + h2 { + margin: 0 0 8px 0; + font-size: 1.2rem; + color: var(--primary-text-color); + } + p { + margin: 0; + color: var(--secondary-text-color); + font-size: 0.9rem; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "dashboard-card": DashboardCard; + } +} diff --git a/src/panels/config/dashboard/dialog-new-dashboard.ts b/src/panels/config/dashboard/dialog-new-dashboard.ts index 5c30a2ccbd..7a736d080a 100644 --- a/src/panels/config/dashboard/dialog-new-dashboard.ts +++ b/src/panels/config/dashboard/dialog-new-dashboard.ts @@ -1,38 +1,71 @@ -import { mdiHome, mdiMap, mdiPencilOutline, mdiShape, mdiWeb } from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import type { IFuseOptions } from "fuse.js"; +import Fuse from "fuse.js"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; -import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import { createCloseHeading } from "../../../components/ha-dialog"; -import "../../../components/ha-icon-next"; -import "../../../components/ha-list-item"; -import "../../../components/ha-list"; +import "../../../components/search-input"; import type { LovelaceRawConfig } from "../../../data/lovelace/config/types"; import type { HassDialog } from "../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import type { NewDashboardDialogParams } from "./show-dialog-new-dashboard"; +import "./dashboard-card"; +import type { LocalizeKeys } from "../../../common/translations/localize"; const EMPTY_CONFIG: LovelaceRawConfig = { views: [{ title: "Home" }] }; interface Strategy { type: string; - iconPath: string; + images: { dark: string; light: string }; + name: string; + description: string; } const STRATEGIES = [ { - type: "map", - iconPath: mdiMap, - }, - { - type: "iframe", - iconPath: mdiWeb, + type: "default", + images: { + light: + "/static/images/dashboard-options/light/icon-dashboard-default.svg", + dark: "/static/images/dashboard-options/dark/icon-dashboard-default.svg", + }, + name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.default.title", + description: + "ui.panel.config.lovelace.dashboards.dialog_new.strategy.default.description", }, { type: "areas", - iconPath: mdiHome, + images: { + light: "/static/images/dashboard-options/light/icon-dashboard-areas.svg", + dark: "/static/images/dashboard-options/dark/icon-dashboard-areas.svg", + }, + name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.areas.title", + description: + "ui.panel.config.lovelace.dashboards.dialog_new.strategy.areas.description", + }, + { + type: "map", + images: { + light: "/static/images/dashboard-options/light/icon-dashboard-map.svg", + dark: "/static/images/dashboard-options/dark/icon-dashboard-map.svg", + }, + name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.map.title", + description: + "ui.panel.config.lovelace.dashboards.dialog_new.strategy.map.description", + }, + { + type: "iframe", + images: { + light: + "/static/images/dashboard-options/light/icon-dashboard-webpage.svg", + dark: "/static/images/dashboard-options/dark/icon-dashboard-webpage.svg", + }, + name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.iframe.title", + description: + "ui.panel.config.lovelace.dashboards.dialog_new.strategy.iframe.description", }, ] as const satisfies Strategy[]; @@ -44,9 +77,23 @@ class DialogNewDashboard extends LitElement implements HassDialog { @state() private _params?: NewDashboardDialogParams; + @state() private _filter = ""; + + @state() private _localizedStrategies: (Strategy & { + localizedName: string; + localizedDescription: string; + })[] = []; + public showDialog(params: NewDashboardDialogParams): void { this._opened = true; this._params = params; + this._localizedStrategies = STRATEGIES.map((strategy) => ({ + ...strategy, + localizedName: this.hass.localize(strategy.name as LocalizeKeys), + localizedDescription: this.hass.localize( + strategy.description as LocalizeKeys + ), + })); } public closeDialog() { @@ -75,83 +122,113 @@ class DialogNewDashboard extends LitElement implements HassDialog { ) )} > - - - - ${this.hass.localize( - `ui.panel.config.lovelace.dashboards.dialog_new.create_empty` - )} - - ${this.hass.localize( - `ui.panel.config.lovelace.dashboards.dialog_new.create_empty_description` - )} - - - -
  • - - - ${this.hass.localize( - `ui.panel.config.lovelace.dashboards.dialog_new.default` - )} - ${this.hass.localize( - `ui.panel.config.lovelace.dashboards.dialog_new.default_description` - )} - - - ${STRATEGIES.map( - (strategy) => html` - - - ${this.hass.localize( - `ui.panel.config.lovelace.dashboards.dialog_new.strategy.${strategy.type}.title` - )} - - ${this.hass.localize( - `ui.panel.config.lovelace.dashboards.dialog_new.strategy.${strategy.type}.description` + .filter=${this._filter} + @value-changed=${this._handleSearchChange} + > +
    + ${this._filter + ? html` +
    + ${this._filterStrategies( + this._localizedStrategies, + this._filter + ).map( + (strategy) => html` + + ` )} - - - - ` - )} - +
    + ` + : html` +
    + +
    +
    +
    + ${this.hass.localize( + `ui.panel.config.lovelace.dashboards.dialog_new.heading.default` + )} +
    + ${this._localizedStrategies.map( + (strategy) => html` + + ` + )} +
    + `} +
    `; } + private _handleSearchChange(ev: CustomEvent) { + this._filter = ev.detail.value; + } + + private _filterStrategies = memoizeOne( + ( + strategies: (Strategy & { + localizedName: string; + localizedDescription: string; + })[], + filter?: string + ): readonly (Strategy & { + localizedName: string; + localizedDescription: string; + })[] => { + if (!filter) { + return strategies; + } + const options: IFuseOptions<(typeof strategies)[0]> = { + keys: ["type", "localizedName", "localizedDescription"], + isCaseSensitive: false, + threshold: 0.3, + ignoreLocation: true, + minMatchCharLength: Math.min(filter.length, 2), + }; + const fuse = new Fuse(strategies, options); + return fuse.search(filter).map((result) => result.item); + } + ); + private _generateStrategyConfig(strategy: string) { return { strategy: { @@ -160,16 +237,19 @@ class DialogNewDashboard extends LitElement implements HassDialog { }; } - private async _selected(ev) { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - + private async _selected(ev: Event) { const target = ev.currentTarget as any; - const config = - target.config || - (target.strategy && this._generateStrategyConfig(target.strategy)) || - null; + let config: any = null; + + if (target.config) { + config = target.config; + } else if (target.strategy) { + if (target.strategy === "default") { + config = null; + } else { + config = this._generateStrategyConfig(target.strategy); + } + } this._params?.selectConfig(config); this.closeDialog(); @@ -180,17 +260,63 @@ class DialogNewDashboard extends LitElement implements HassDialog { haStyle, haStyleDialog, css` - ha-dialog { - --dialog-content-padding: 0; - --mdc-dialog-max-height: 60vh; - } - @media all and (min-width: 550px) { + @media all and (max-width: 450px), all and (max-height: 500px) { + /* overrule the ha-style-dialog max-height on small screens */ ha-dialog { - --mdc-dialog-min-width: 500px; + --mdc-dialog-max-height: 100%; + height: 100%; } } - ha-icon-next { - width: 24px; + + @media all and (min-width: 850px) { + ha-dialog { + --mdc-dialog-min-width: 845px; + --mdc-dialog-min-height: calc(100vh - 72px); + --mdc-dialog-max-height: calc(100vh - 72px); + } + } + + ha-dialog { + --mdc-dialog-max-width: 845px; + --dialog-content-padding: 0; + --dialog-z-index: 6; + } + .cards-container-header { + font-size: var(--ha-font-size-l); + font-weight: var(--ha-font-weight-medium); + padding: 12px 8px; + margin: 0; + grid-column: 1 / -1; + position: sticky; + top: 56px; + z-index: 1; + background: linear-gradient( + 90deg, + var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)) + 0%, + #ffffff00 80% + ); + } + search-input { + display: block; + --mdc-shape-small: var(--card-picker-search-shape); + margin: var(--card-picker-search-margin); + position: sticky; + top: 0; + z-index: 10; + background-color: var( + --ha-dialog-surface-background, + var(--mdc-theme-surface, #fff) + ); + } + .cards-container { + display: grid; + grid-gap: 8px 8px; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + margin-top: 20px; + } + .content { + padding: 0 24px 0 24px; } `, ]; diff --git a/src/translations/en.json b/src/translations/en.json index 246afb4797..5ef4d2a0b5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3266,7 +3266,15 @@ "areas": { "title": "Areas (experimental)", "description": "Display your devices with a view for each area" + }, + "default": { + "title": "Default dashboard", + "description": "Display your devices grouped by area" } + }, + "search_dashboards": "Search dashboards", + "heading": { + "default": "Dashboards" } }, "picker": {