Compare commits

..

3 Commits

Author SHA1 Message Date
Paul Bottein
02ea05464e Prettier 2025-11-07 17:49:33 +01:00
Paul Bottein
d45b2ec5c1 Don't use state if only one view 2025-11-07 17:48:20 +01:00
Paul Bottein
1a14e08f12 Create dedicated panel for home dashboard 2025-11-07 17:42:39 +01:00
8 changed files with 756 additions and 315 deletions

View File

@@ -35,6 +35,7 @@ const COMPONENTS = {
light: () => import("../panels/light/ha-panel-light"),
security: () => import("../panels/security/ha-panel-security"),
climate: () => import("../panels/climate/ha-panel-climate"),
home: () => import("../panels/home/ha-panel-home"),
};
@customElement("partial-panel-resolver")

View File

@@ -11,8 +11,7 @@ import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import "../lovelace/hui-lovelace";
const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: {
@@ -26,8 +25,6 @@ class PanelClimate extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _viewIndex = 0;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
@@ -92,40 +89,33 @@ class PanelClimate extends LitElement {
return html`
<div class="header">
<div class="toolbar">
${
this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`
}
${this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
<div class="main-title">${this.hass.localize("panel.climate")}</div>
</div>
</div>
${
this._lovelace
? html`
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view
></hui-view-container>
`
: nothing
}
</hui-view-container>
${this._lovelace
? html`
<hui-lovelace
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.curView=${0}
></hui-lovelace>
`
: nothing}
`;
}
@@ -143,8 +133,8 @@ class PanelClimate extends LitElement {
}
this._lovelace = {
config: config,
rawConfig: rawConfig,
config,
rawConfig,
editMode: false,
urlPath: "climate",
mode: "generated",
@@ -223,7 +213,7 @@ class PanelClimate extends LitElement {
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
hui-view-container {
hui-lovelace {
position: relative;
display: flex;
min-height: 100vh;
@@ -233,14 +223,10 @@ class PanelClimate extends LitElement {
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-view-container {
:host([narrow]) hui-lovelace {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
`,
];
}

View File

@@ -0,0 +1,393 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { goBack, navigate } from "../../common/navigate";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-icon";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import "../../components/ha-tab-group";
import "../../components/ha-tab-group-tab";
import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types";
import { generateLovelaceDashboardStrategy } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/hui-lovelace";
const HOME_LOVELACE_CONFIG: LovelaceDashboardStrategyConfig = {
strategy: {
type: "home",
},
};
@customElement("ha-panel-home")
class PanelHome extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ attribute: false }) public route?: Route;
@state() private _curView?: number;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
// Initial setup
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
this._setLovelace();
return;
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass && oldHass.localize !== this.hass.localize) {
this._setLovelace();
return;
}
if (oldHass && this.hass) {
// If the entity registry changed, ask the user if they want to refresh the config
if (
oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors
) {
if (this.hass.config.state === "RUNNING") {
this._debounceRegistriesChanged();
return;
}
}
// If ha started, refresh the config
if (
this.hass.config.state === "RUNNING" &&
oldHass.config.state !== "RUNNING"
) {
this._setLovelace();
}
}
}
private _debounceRegistriesChanged = debounce(
() => this._registriesChanged(),
200
);
private _registriesChanged = async () => {
this._setLovelace();
};
private _isVisible = (view: LovelaceViewConfig) =>
Boolean(
view.visible === undefined ||
view.visible === true ||
(Array.isArray(view.visible) &&
view.visible.some((show) => show.user === this.hass.user?.id))
);
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (!this._lovelace || !this.route) {
return;
}
let viewPath: string | undefined = this.route.path.split("/")[1];
viewPath = viewPath ? decodeURI(viewPath) : undefined;
if (changedProperties.has("route")) {
const views = this._lovelace.config.views;
if (!viewPath && views.length) {
// No path: navigate to first visible view
const newSelectView = views.findIndex(this._isVisible);
this._navigateToView(views[newSelectView].path || newSelectView, true);
} else if (viewPath) {
// Match by path or index
const selectedView = viewPath;
const selectedViewInt = Number(selectedView);
let index = 0;
for (let i = 0; i < views.length; i++) {
if (views[i].path === selectedView || i === selectedViewInt) {
index = i;
break;
}
}
this._curView = index;
}
}
}
private _navigateToView(path: string | number, replace?: boolean) {
if (!this.route) {
return;
}
const url = `${this.route.prefix}/${path}${location.search}`;
const currentUrl = `${location.pathname}${location.search}`;
if (currentUrl !== url) {
navigate(url, { replace });
}
}
private _handleViewSelected(ev) {
ev.preventDefault();
const viewIndex = Number(ev.detail.name);
if (viewIndex !== this._curView && this._lovelace?.config.views) {
const path = this._lovelace.config.views[viewIndex].path || viewIndex;
this._navigateToView(path);
} else {
scrollTo({ behavior: "smooth", top: 0 });
}
}
private _goBack(): void {
const views = this._lovelace?.config.views ?? [];
const curViewConfig =
typeof this._curView === "number" ? views[this._curView] : undefined;
if (curViewConfig?.back_path != null) {
navigate(curViewConfig.back_path, { replace: true });
} else if (history.length > 1) {
goBack();
} else if (views[0] && !views[0].subview) {
navigate(this.route!.prefix, { replace: true });
} else {
navigate("/");
}
}
protected render() {
if (!this._lovelace) {
return nothing;
}
const views = this._lovelace.config.views;
const curViewConfig =
typeof this._curView === "number" ? views[this._curView] : undefined;
// Helper function to determine if a tab should be hidden for user
const _isTabHiddenForUser = (view: LovelaceViewConfig) =>
view.visible === false ||
(Array.isArray(view.visible) &&
!view.visible.some((show) => show.user === this.hass.user?.id));
const tabs = html`<ha-tab-group @wa-tab-show=${this._handleViewSelected}>
${views.map((view, index) => {
const hidden = view.subview || _isTabHiddenForUser(view);
return html`
<ha-tab-group-tab
slot="nav"
panel=${index}
.active=${this._curView === index}
.disabled=${hidden}
aria-label=${ifDefined(view.title)}
class=${classMap({
icon: Boolean(view.icon),
"hide-tab": Boolean(hidden),
})}
>
${view.icon
? html`<ha-icon
class=${classMap({
"child-view-icon": Boolean(view.subview),
})}
title=${ifDefined(view.title)}
.icon=${view.icon}
></ha-icon>`
: view.title ||
this.hass.localize("ui.panel.lovelace.views.unnamed_view")}
</ha-tab-group-tab>
`;
})}
</ha-tab-group>`;
const isSubview = curViewConfig?.subview;
const hasTabViews = views.filter((view) => !view.subview).length > 1;
return html`
<div class="header">
<div class="toolbar">
${this._searchParms.has("historyBack") || isSubview
? html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
slot="navigationIcon"
@click=${this._goBack}
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
${isSubview
? html`<div class="main-title">${curViewConfig.title}</div>`
: hasTabViews
? tabs
: html`<div class="main-title">
${views[0]?.title ?? this.hass.localize("panel.home")}
</div>`}
</div>
</div>
<hui-lovelace
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.curView=${this._curView}
></hui-lovelace>
`;
}
private async _setLovelace() {
const config = await generateLovelaceDashboardStrategy(
HOME_LOVELACE_CONFIG,
this.hass
);
const rawConfig = HOME_LOVELACE_CONFIG;
if (deepEqual(config, this._lovelace?.config)) {
return;
}
this._lovelace = {
config: config,
rawConfig: rawConfig,
editMode: false,
urlPath: "home",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
position: fixed;
top: 0;
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
}
:host([narrow]) .header {
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
:host([scrolled]) .header {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.toolbar {
height: var(--header-height);
display: flex;
flex: 1;
align-items: center;
font-size: var(--ha-font-size-xl);
padding: 0px 12px;
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
}
:host([narrow]) .toolbar {
padding: 0 4px;
}
.main-title {
margin: var(--margin-title);
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
ha-tab-group {
margin-left: 12px;
margin-inline-start: 12px;
margin-inline-end: initial;
flex: 1;
max-width: 100%;
}
ha-tab-group-tab {
--mdc-icon-size: 20px;
max-width: 200px;
}
ha-tab-group-tab.icon {
height: 48px;
}
ha-tab-group-tab.hide-tab {
display: none;
}
.child-view-icon {
font-size: 16px;
}
hui-lovelace {
position: relative;
display: flex;
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-lovelace {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-panel-home": PanelHome;
}
}

View File

@@ -11,8 +11,7 @@ import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import "../lovelace/hui-lovelace";
const LIGHT_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: {
@@ -26,8 +25,6 @@ class PanelLight extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _viewIndex = 0;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
@@ -92,40 +89,33 @@ class PanelLight extends LitElement {
return html`
<div class="header">
<div class="toolbar">
${
this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`
}
${this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
<div class="main-title">${this.hass.localize("panel.light")}</div>
</div>
</div>
${
this._lovelace
? html`
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view
></hui-view-container>
`
: nothing
}
</hui-view-container>
${this._lovelace
? html`
<hui-lovelace
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.curView=${0}
></hui-lovelace>
`
: nothing}
`;
}
@@ -143,8 +133,8 @@ class PanelLight extends LitElement {
}
this._lovelace = {
config: config,
rawConfig: rawConfig,
config,
rawConfig,
editMode: false,
urlPath: "light",
mode: "generated",
@@ -223,7 +213,7 @@ class PanelLight extends LitElement {
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
hui-view-container {
hui-lovelace {
position: relative;
display: flex;
min-height: 100vh;
@@ -233,14 +223,10 @@ class PanelLight extends LitElement {
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-view-container {
:host([narrow]) hui-lovelace {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
`,
];
}

View File

@@ -0,0 +1,236 @@
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { afterNextRender } from "../../common/util/render-status";
import { debounce } from "../../common/util/debounce";
import type { HomeAssistant } from "../../types";
import type { Lovelace } from "./types";
import "./views/hui-view";
import type { HUIView } from "./views/hui-view";
import "./views/hui-view-background";
import "./views/hui-view-container";
@customElement("hui-lovelace")
export class HUILovelace extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public lovelace!: Lovelace;
@property({ attribute: false, type: Number }) public curView?: number;
private _displayedView?: number;
private _viewCache?: Record<number, HUIView>;
private _viewScrollPositions: Record<number, number> = {};
private _restoreScroll = false;
private _debouncedConfigChanged = debounce(
() => this._selectView(this._displayedView, true),
100,
false
);
private get _viewRoot(): HTMLDivElement {
return this.shadowRoot!.getElementById("view") as HTMLDivElement;
}
private _handleWindowScroll = () => {
this.toggleAttribute("scrolled", window.scrollY !== 0);
};
private _handlePopState = () => {
this._restoreScroll = true;
};
public connectedCallback(): void {
super.connectedCallback();
window.addEventListener("scroll", this._handleWindowScroll, {
passive: true,
});
window.addEventListener("popstate", this._handlePopState);
// Disable history scroll restoration because it is managed manually here
window.history.scrollRestoration = "manual";
}
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener("scroll", this._handleWindowScroll);
window.removeEventListener("popstate", this._handlePopState);
this.toggleAttribute("scrolled", window.scrollY !== 0);
// Re-enable history scroll restoration when leaving the page
window.history.scrollRestoration = "auto";
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (!this.lovelace) {
return;
}
const view = this._viewRoot;
const huiView = view?.lastChild as HUIView | undefined;
if (changedProperties.has("hass") && huiView) {
huiView.hass = this.hass;
}
if (changedProperties.has("narrow") && huiView) {
huiView.narrow = this.narrow;
}
let newSelectView: number | undefined;
let force = false;
if (changedProperties.has("curView")) {
newSelectView = this.curView;
}
if (changedProperties.has("lovelace")) {
const oldLovelace = changedProperties.get("lovelace") as
| Lovelace
| undefined;
if (!oldLovelace || oldLovelace.config !== this.lovelace.config) {
// On config change, recreate the current view from scratch.
force = true;
newSelectView = this.curView;
}
if (!force && huiView) {
huiView.lovelace = this.lovelace;
}
}
if (newSelectView !== undefined || force) {
if (force && newSelectView === undefined) {
newSelectView = this.curView;
}
// Will allow for ripples to start rendering
afterNextRender(() => {
if (changedProperties.has("curView")) {
const position =
(this._restoreScroll &&
newSelectView !== undefined &&
this._viewScrollPositions[newSelectView]) ||
0;
this._restoreScroll = false;
requestAnimationFrame(() =>
scrollTo({ behavior: "auto", top: position })
);
}
this._selectView(newSelectView, force);
});
}
}
private _selectView(viewIndex: number | undefined, force: boolean): void {
if (!force && this._displayedView === viewIndex) {
return;
}
// Save scroll position of current view
if (this._displayedView != null) {
this._viewScrollPositions[this._displayedView] = window.scrollY;
}
viewIndex = viewIndex === undefined ? 0 : viewIndex;
this._displayedView = viewIndex;
if (force) {
this._viewCache = {};
this._viewScrollPositions = {};
}
// Recreate a new element to clear the applied themes.
const root = this._viewRoot;
if (root.lastChild) {
root.removeChild(root.lastChild);
}
if (!this.lovelace) {
return;
}
const viewConfig = this.lovelace.config.views[viewIndex];
if (!viewConfig) {
return;
}
let view: HUIView;
// Use cached view if available
if (!force && this._viewCache![viewIndex]) {
view = this._viewCache![viewIndex];
} else {
view = document.createElement("hui-view");
view.index = viewIndex;
this._viewCache![viewIndex] = view;
}
view.lovelace = this.lovelace;
view.hass = this.hass;
view.narrow = this.narrow;
root.appendChild(view);
}
protected render() {
if (!this.lovelace) {
return nothing;
}
const curViewConfig =
this.curView !== undefined
? this.lovelace.config.views[this.curView]
: undefined;
const background =
curViewConfig?.background || this.lovelace.config.background;
return html`
<hui-view-container
.hass=${this.hass}
.theme=${curViewConfig?.theme}
@ll-rebuild=${this._debouncedConfigChanged}
>
<div id="view"></div>
</hui-view-container>
<hui-view-background .hass=${this.hass} .background=${background}>
</hui-view-background>
`;
}
static styles = css`
:host {
display: flex;
flex: 1;
}
hui-view-container {
flex: 1;
display: flex;
}
#view {
flex: 1 1 100%;
max-width: 100%;
display: flex;
}
#view > * {
flex: 1 1 100%;
max-width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-lovelace": HUILovelace;
}
}

View File

@@ -34,7 +34,6 @@ import {
extractSearchParamsObject,
removeSearchParam,
} from "../../common/url/search-params";
import { debounce } from "../../common/util/debounce";
import { afterNextRender } from "../../common/util/render-status";
import "../../components/ha-button";
import "../../components/ha-button-menu";
@@ -88,8 +87,8 @@ import { showEditViewDialog } from "./editor/view-editor/show-edit-view-dialog";
import { getLovelaceStrategy } from "./strategies/get-strategy";
import { isLegacyStrategyConfig } from "./strategies/legacy-strategy";
import type { Lovelace } from "./types";
import "./views/hui-view";
import type { HUIView } from "./views/hui-view";
import "./editor/unused-entities/hui-unused-entities";
import "./hui-lovelace";
import "./views/hui-view-background";
import "./views/hui-view-container";
@@ -130,29 +129,10 @@ class HUIRoot extends LitElement {
@state() private _curView?: number | "hass-unused-entities";
private _viewCache?: Record<string, HUIView>;
private _viewScrollPositions: Record<string, number> = {};
private _restoreScroll = false;
private _debouncedConfigChanged: () => void;
private _conversation = memoizeOne((_components) =>
isComponentLoaded(this.hass, "conversation")
);
constructor() {
super();
// The view can trigger a re-render when it knows that certain
// web components have been loaded.
this._debouncedConfigChanged = debounce(
() => this._selectView(this._curView, true),
100,
false
);
}
private _renderActionItems(): TemplateResult {
const result: TemplateResult[] = [];
if (this._editMode) {
@@ -411,8 +391,6 @@ class HUIRoot extends LitElement {
? getPanelTitle(this.hass, this.panel)
: undefined;
const background = curViewConfig?.background || this.config.background;
const _isTabHiddenForUser = (view: LovelaceViewConfig) =>
view.visible !== undefined &&
((Array.isArray(view.visible) &&
@@ -560,28 +538,31 @@ class HUIRoot extends LitElement {
`
: nothing}
</div>
<hui-view-container
class=${this._editMode ? "has-tab-bar" : ""}
.hass=${this.hass}
.theme=${curViewConfig?.theme}
id="view"
@ll-rebuild=${this._debouncedConfigChanged}
>
<hui-view-background .hass=${this.hass} .background=${background}>
</hui-view-background>
</hui-view-container>
${this._curView === "hass-unused-entities"
? html`
<hui-view-container .hass=${this.hass}>
<hui-unused-entities
.hass=${this.hass}
.lovelace=${this.lovelace}
.narrow=${this.narrow}
></hui-unused-entities>
</hui-view-container>
`
: html`
<hui-lovelace
class=${this._editMode ? "has-tab-bar" : ""}
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this.lovelace}
.curView=${typeof this._curView === "number"
? this._curView
: 0}
></hui-lovelace>
`}
</div>
`;
}
private _handleWindowScroll = () => {
this.toggleAttribute("scrolled", window.scrollY !== 0);
};
private _handlePopState = () => {
this._restoreScroll = true;
};
private _isVisible = (view: LovelaceViewConfig) =>
Boolean(
this._editMode ||
@@ -620,59 +601,26 @@ class HUIRoot extends LitElement {
this._showMoreInfoDialog(entityId);
});
}
window.addEventListener("scroll", this._handleWindowScroll, {
passive: true,
});
}
public connectedCallback(): void {
super.connectedCallback();
window.addEventListener("scroll", this._handleWindowScroll, {
passive: true,
});
window.addEventListener("popstate", this._handlePopState);
// Disable history scroll restoration because it is managed manually here
window.history.scrollRestoration = "manual";
}
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener("scroll", this._handleWindowScroll);
window.removeEventListener("popstate", this._handlePopState);
this.toggleAttribute("scrolled", window.scrollY !== 0);
// Re-enable history scroll restoration when leaving the page
window.history.scrollRestoration = "auto";
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
const view = this._viewRoot;
const huiView = view.lastChild as HUIView;
if (changedProperties.has("hass") && huiView) {
huiView.hass = this.hass;
if (!this.route || !this.lovelace) {
return;
}
if (changedProperties.has("narrow") && huiView) {
huiView.narrow = this.narrow;
}
let newSelectView;
let force = false;
let viewPath: string | undefined = this.route!.path.split("/")[1];
let viewPath: string | undefined = this.route.path.split("/")[1];
viewPath = viewPath ? decodeURI(viewPath) : undefined;
if (changedProperties.has("route")) {
const views = this.config.views;
if (!viewPath && views.length) {
newSelectView = views.findIndex(this._isVisible);
const newSelectView = views.findIndex(this._isVisible);
this._navigateToView(views[newSelectView].path || newSelectView, true);
} else if (viewPath === "hass-unused-entities") {
newSelectView = "hass-unused-entities";
this._curView = "hass-unused-entities";
} else if (viewPath) {
const selectedView = viewPath;
const selectedViewInt = Number(selectedView);
@@ -683,7 +631,7 @@ class HUIRoot extends LitElement {
break;
}
}
newSelectView = index;
this._curView = index;
}
}
@@ -692,49 +640,21 @@ class HUIRoot extends LitElement {
| Lovelace
| undefined;
if (!oldLovelace || oldLovelace.config !== this.lovelace!.config) {
// On config change, recreate the current view from scratch.
force = true;
}
if (!oldLovelace || oldLovelace.editMode !== this.lovelace!.editMode) {
if (!oldLovelace || oldLovelace.editMode !== this.lovelace.editMode) {
const views = this.config && this.config.views;
// Leave unused entities when leaving edit mode
if (
this.lovelace!.mode === "storage" &&
this.lovelace.mode === "storage" &&
viewPath === "hass-unused-entities"
) {
newSelectView = views.findIndex(this._isVisible);
const newSelectView = views.findIndex(this._isVisible);
this._navigateToView(
views[newSelectView].path || newSelectView,
true
);
}
}
if (!force && huiView) {
huiView.lovelace = this.lovelace!;
}
}
if (newSelectView !== undefined || force) {
if (force && newSelectView === undefined) {
newSelectView = this._curView;
}
// Will allow for ripples to start rendering
afterNextRender(() => {
if (changedProperties.has("route")) {
const position =
(this._restoreScroll && this._viewScrollPositions[newSelectView]) ||
0;
this._restoreScroll = false;
requestAnimationFrame(() =>
scrollTo({ behavior: "auto", top: position })
);
}
this._selectView(newSelectView, force);
});
}
}
@@ -750,10 +670,6 @@ class HUIRoot extends LitElement {
return this.lovelace!.editMode;
}
private get _viewRoot(): HTMLDivElement {
return this.shadowRoot!.getElementById("view") as HTMLDivElement;
}
private _handleRefresh(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
@@ -1141,67 +1057,6 @@ class HUIRoot extends LitElement {
}
}
private _selectView(viewIndex: HUIRoot["_curView"], force: boolean): void {
if (!force && this._curView === viewIndex) {
return;
}
// Save scroll position of current view
if (this._curView != null) {
this._viewScrollPositions[this._curView] = window.scrollY;
}
viewIndex = viewIndex === undefined ? 0 : viewIndex;
this._curView = viewIndex;
if (force) {
this._viewCache = {};
this._viewScrollPositions = {};
}
// Recreate a new element to clear the applied themes.
const root = this._viewRoot;
if (root.lastChild) {
root.removeChild(root.lastChild);
}
if (viewIndex === "hass-unused-entities") {
const unusedEntities = document.createElement("hui-unused-entities");
// Wait for promise to resolve so that the element has been upgraded.
import("./editor/unused-entities/hui-unused-entities").then(() => {
unusedEntities.hass = this.hass!;
unusedEntities.lovelace = this.lovelace!;
unusedEntities.narrow = this.narrow;
});
root.appendChild(unusedEntities);
return;
}
let view;
const viewConfig = this.config.views[viewIndex];
if (!viewConfig) {
this.lovelace!.setEditMode(true);
return;
}
if (!force && this._viewCache![viewIndex]) {
view = this._viewCache![viewIndex];
} else {
view = document.createElement("hui-view");
view.index = viewIndex;
this._viewCache![viewIndex] = view;
}
view.lovelace = this.lovelace;
view.hass = this.hass;
view.narrow = this.narrow;
root.appendChild(view);
}
private _openShortcutDialog(ev: Event) {
ev.preventDefault();
showShortcutsDialog(this);
@@ -1395,6 +1250,7 @@ class HUIRoot extends LitElement {
a {
color: var(--text-primary-color, white);
}
hui-lovelace,
hui-view-container {
position: relative;
display: flex;
@@ -1405,21 +1261,17 @@ class HUIRoot extends LitElement {
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
.narrow hui-lovelace,
.narrow hui-view-container {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view-container > * {
flex: 1 1 100%;
max-width: 100%;
}
/**
* In edit mode we have the tab bar on a new line *
* In edit mode we have the tab bar on a new line
*/
hui-view-container.has-tab-bar {
hui-lovelace.has-tab-bar {
padding-top: calc(
var(--header-height, 56px) +
calc(var(--tab-bar-height, 56px) - 2px) +
var(--header-height, 56px) + var(--tab-bar-height, 56px) - 2px +
var(--safe-area-inset-top, 0px)
);
}

View File

@@ -11,8 +11,7 @@ import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import "../lovelace/hui-lovelace";
const SECURITY_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: {
@@ -26,8 +25,6 @@ class PanelSecurity extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _viewIndex = 0;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
@@ -92,40 +89,33 @@ class PanelSecurity extends LitElement {
return html`
<div class="header">
<div class="toolbar">
${
this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`
}
${this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
<div class="main-title">${this.hass.localize("panel.security")}</div>
</div>
</div>
${
this._lovelace
? html`
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view
></hui-view-container>
`
: nothing
}
</hui-view-container>
${this._lovelace
? html`
<hui-lovelace
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.curView=${0}
></hui-lovelace>
`
: nothing}
`;
}
@@ -143,8 +133,8 @@ class PanelSecurity extends LitElement {
}
this._lovelace = {
config: config,
rawConfig: rawConfig,
config,
rawConfig,
editMode: false,
urlPath: "security",
mode: "generated",
@@ -223,7 +213,7 @@ class PanelSecurity extends LitElement {
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
hui-view-container {
hui-lovelace {
position: relative;
display: flex;
min-height: 100vh;
@@ -233,14 +223,10 @@ class PanelSecurity extends LitElement {
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-view-container {
:host([narrow]) hui-lovelace {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
`,
];
}

View File

@@ -13,7 +13,8 @@
"profile": "Profile",
"light": "Lights",
"security": "Security",
"climate": "Climate"
"climate": "Climate",
"home": "Home"
},
"state": {
"default": {