mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-20 02:38:53 +00:00
Add sidebar to home dashboard (#28084)
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
import type { MediaSelectorValue } from "../../selector";
|
import type { MediaSelectorValue } from "../../selector";
|
||||||
import type { LovelaceBadgeConfig } from "./badge";
|
import type { LovelaceBadgeConfig } from "./badge";
|
||||||
import type { LovelaceCardConfig } from "./card";
|
import type { LovelaceCardConfig } from "./card";
|
||||||
import type { LovelaceSectionRawConfig } from "./section";
|
import type {
|
||||||
|
LovelaceSectionConfig,
|
||||||
|
LovelaceSectionRawConfig,
|
||||||
|
} from "./section";
|
||||||
import type { LovelaceStrategyConfig } from "./strategy";
|
import type { LovelaceStrategyConfig } from "./strategy";
|
||||||
|
|
||||||
export interface ShowViewConfig {
|
export interface ShowViewConfig {
|
||||||
@@ -33,6 +36,12 @@ export interface LovelaceViewHeaderConfig {
|
|||||||
badges_wrap?: "wrap" | "scroll";
|
badges_wrap?: "wrap" | "scroll";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LovelaceViewSidebarConfig {
|
||||||
|
sections?: LovelaceSectionConfig[];
|
||||||
|
content_label?: string;
|
||||||
|
sidebar_label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LovelaceBaseViewConfig {
|
export interface LovelaceBaseViewConfig {
|
||||||
index?: number;
|
index?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -56,6 +65,8 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
|
|||||||
cards?: LovelaceCardConfig[];
|
cards?: LovelaceCardConfig[];
|
||||||
sections?: LovelaceSectionRawConfig[];
|
sections?: LovelaceSectionRawConfig[];
|
||||||
header?: LovelaceViewHeaderConfig;
|
header?: LovelaceViewHeaderConfig;
|
||||||
|
// Only used for section view, it should move to a section view config type when the views will have dedicated editor.
|
||||||
|
sidebar?: LovelaceViewSidebarConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig {
|
export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
|
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
|
||||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||||
import {
|
import {
|
||||||
findEntities,
|
findEntities,
|
||||||
@@ -23,8 +24,8 @@ import type {
|
|||||||
WeatherForecastCardConfig,
|
WeatherForecastCardConfig,
|
||||||
} from "../../cards/types";
|
} from "../../cards/types";
|
||||||
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
|
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
|
||||||
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
|
|
||||||
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||||
|
import type { Condition } from "../../common/validate-condition";
|
||||||
|
|
||||||
export interface HomeOverviewViewStrategyConfig {
|
export interface HomeOverviewViewStrategyConfig {
|
||||||
type: "home-overview";
|
type: "home-overview";
|
||||||
@@ -70,8 +71,12 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
|||||||
|
|
||||||
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
|
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
|
||||||
|
|
||||||
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
|
const maxColumns = 3;
|
||||||
const maxColumns = 2;
|
|
||||||
|
const largeScreenCondition: Condition = {
|
||||||
|
condition: "screen",
|
||||||
|
media_query: "(min-width: 871px)",
|
||||||
|
};
|
||||||
|
|
||||||
const floorsSections: LovelaceSectionConfig[] = [];
|
const floorsSections: LovelaceSectionConfig[] = [];
|
||||||
for (const floorStructure of home.floors) {
|
for (const floorStructure of home.floors) {
|
||||||
@@ -126,12 +131,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const favoriteSection: LovelaceSectionConfig = {
|
|
||||||
type: "grid",
|
|
||||||
column_span: maxColumns,
|
|
||||||
cards: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const favoriteEntities = (config.favorite_entities || []).filter(
|
const favoriteEntities = (config.favorite_entities || []).filter(
|
||||||
(entityId) => hass.states[entityId] !== undefined
|
(entityId) => hass.states[entityId] !== undefined
|
||||||
);
|
);
|
||||||
@@ -176,74 +175,70 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
|||||||
({
|
({
|
||||||
type: "home-summary",
|
type: "home-summary",
|
||||||
summary: "light",
|
summary: "light",
|
||||||
vertical: true,
|
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "navigate",
|
action: "navigate",
|
||||||
navigation_path: "/light?historyBack=1",
|
navigation_path: "/light?historyBack=1",
|
||||||
},
|
},
|
||||||
grid_options: {
|
grid_options: {
|
||||||
rows: 2,
|
columns: 12,
|
||||||
columns: 4,
|
|
||||||
},
|
},
|
||||||
} satisfies HomeSummaryCard),
|
} satisfies HomeSummaryCard),
|
||||||
hasClimate &&
|
hasClimate &&
|
||||||
({
|
({
|
||||||
type: "home-summary",
|
type: "home-summary",
|
||||||
summary: "climate",
|
summary: "climate",
|
||||||
vertical: true,
|
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "navigate",
|
action: "navigate",
|
||||||
navigation_path: "/climate?historyBack=1",
|
navigation_path: "/climate?historyBack=1",
|
||||||
},
|
},
|
||||||
grid_options: {
|
grid_options: {
|
||||||
rows: 2,
|
columns: 12,
|
||||||
columns: 4,
|
|
||||||
},
|
},
|
||||||
} satisfies HomeSummaryCard),
|
} satisfies HomeSummaryCard),
|
||||||
hasSecurity &&
|
hasSecurity &&
|
||||||
({
|
({
|
||||||
type: "home-summary",
|
type: "home-summary",
|
||||||
summary: "security",
|
summary: "security",
|
||||||
vertical: true,
|
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "navigate",
|
action: "navigate",
|
||||||
navigation_path: "/security?historyBack=1",
|
navigation_path: "/security?historyBack=1",
|
||||||
},
|
},
|
||||||
grid_options: {
|
grid_options: {
|
||||||
rows: 2,
|
columns: 12,
|
||||||
columns: 4,
|
|
||||||
},
|
},
|
||||||
} satisfies HomeSummaryCard),
|
} satisfies HomeSummaryCard),
|
||||||
hasMediaPlayers &&
|
hasMediaPlayers &&
|
||||||
({
|
({
|
||||||
type: "home-summary",
|
type: "home-summary",
|
||||||
summary: "media_players",
|
summary: "media_players",
|
||||||
vertical: true,
|
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "navigate",
|
action: "navigate",
|
||||||
navigation_path: "media-players",
|
navigation_path: "media-players",
|
||||||
},
|
},
|
||||||
grid_options: {
|
grid_options: {
|
||||||
rows: 2,
|
columns: 12,
|
||||||
columns: 4,
|
|
||||||
},
|
},
|
||||||
} satisfies HomeSummaryCard),
|
} satisfies HomeSummaryCard),
|
||||||
].filter(Boolean) as LovelaceCardConfig[];
|
].filter(Boolean) as LovelaceCardConfig[];
|
||||||
|
|
||||||
const summarySection: LovelaceSectionConfig = {
|
const forYouSection: LovelaceSectionConfig = {
|
||||||
type: "grid",
|
type: "grid",
|
||||||
column_span: maxColumns,
|
cards: [
|
||||||
|
{
|
||||||
|
type: "heading",
|
||||||
|
heading: hass.localize("ui.panel.lovelace.strategy.home.for_you"),
|
||||||
|
heading_style: "title",
|
||||||
|
visibility: [largeScreenCondition],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const widgetSection: LovelaceSectionConfig = {
|
||||||
cards: [],
|
cards: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (summaryCards.length) {
|
if (summaryCards.length) {
|
||||||
summarySection.cards!.push(
|
widgetSection.cards!.push(...summaryCards);
|
||||||
{
|
|
||||||
type: "heading",
|
|
||||||
heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"),
|
|
||||||
},
|
|
||||||
...summaryCards
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const weatherFilter = generateEntityFilter(hass, {
|
const weatherFilter = generateEntityFilter(hass, {
|
||||||
@@ -251,28 +246,16 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
|||||||
entity_category: "none",
|
entity_category: "none",
|
||||||
});
|
});
|
||||||
|
|
||||||
const widgetSection: LovelaceSectionConfig = {
|
|
||||||
type: "grid",
|
|
||||||
column_span: maxColumns,
|
|
||||||
cards: [],
|
|
||||||
};
|
|
||||||
const weatherEntity = Object.keys(hass.states)
|
const weatherEntity = Object.keys(hass.states)
|
||||||
.filter(weatherFilter)
|
.filter(weatherFilter)
|
||||||
.sort()[0];
|
.sort()[0];
|
||||||
|
|
||||||
if (weatherEntity) {
|
if (weatherEntity) {
|
||||||
widgetSection.cards!.push(
|
widgetSection.cards!.push({
|
||||||
{
|
type: "weather-forecast",
|
||||||
type: "heading",
|
entity: weatherEntity,
|
||||||
heading: "",
|
forecast_type: "daily",
|
||||||
heading_style: "subtitle",
|
} as WeatherForecastCardConfig);
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "weather-forecast",
|
|
||||||
entity: weatherEntity,
|
|
||||||
forecast_type: "daily",
|
|
||||||
} as WeatherForecastCardConfig
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const energyPrefs = isComponentLoaded(hass, "energy")
|
const energyPrefs = isComponentLoaded(hass, "energy")
|
||||||
@@ -299,11 +282,19 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
|||||||
|
|
||||||
const sections = (
|
const sections = (
|
||||||
[
|
[
|
||||||
favoriteSection.cards && favoriteSection,
|
{
|
||||||
|
type: "grid",
|
||||||
|
cards: [
|
||||||
|
// Heading to add some spacing on large screens
|
||||||
|
{
|
||||||
|
type: "heading",
|
||||||
|
heading_style: "subtitle",
|
||||||
|
visibility: [largeScreenCondition],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
commonControlsSection,
|
commonControlsSection,
|
||||||
summarySection.cards && summarySection,
|
|
||||||
...floorsSections,
|
...floorsSections,
|
||||||
widgetSection.cards && widgetSection,
|
|
||||||
] satisfies (LovelaceSectionRawConfig | undefined)[]
|
] satisfies (LovelaceSectionRawConfig | undefined)[]
|
||||||
).filter(Boolean) as LovelaceSectionRawConfig[];
|
).filter(Boolean) as LovelaceSectionRawConfig[];
|
||||||
|
|
||||||
@@ -319,6 +310,11 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
|||||||
content: `## ${hass.localize("ui.panel.lovelace.strategy.home.welcome_user", { user: "{{ user }}" })}`,
|
content: `## ${hass.localize("ui.panel.lovelace.strategy.home.welcome_user", { user: "{{ user }}" })}`,
|
||||||
} satisfies MarkdownCardConfig,
|
} satisfies MarkdownCardConfig,
|
||||||
},
|
},
|
||||||
|
sidebar: {
|
||||||
|
sections: [forYouSection, widgetSection],
|
||||||
|
content_label: hass.localize("ui.panel.lovelace.strategy.home.home"),
|
||||||
|
sidebar_label: hass.localize("ui.panel.lovelace.strategy.home.for_you"),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
import type { HuiSection } from "../sections/hui-section";
|
import type { HuiSection } from "../sections/hui-section";
|
||||||
import type { Lovelace } from "../types";
|
import type { Lovelace } from "../types";
|
||||||
import "./hui-view-header";
|
import "./hui-view-header";
|
||||||
|
import "./hui-view-sidebar";
|
||||||
|
|
||||||
export const DEFAULT_MAX_COLUMNS = 4;
|
export const DEFAULT_MAX_COLUMNS = 4;
|
||||||
|
|
||||||
@@ -46,6 +47,8 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public isStrategy = false;
|
@property({ attribute: false }) public isStrategy = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public sections: HuiSection[] = [];
|
@property({ attribute: false }) public sections: HuiSection[] = [];
|
||||||
|
|
||||||
@property({ attribute: false }) public cards: HuiCard[] = [];
|
@property({ attribute: false }) public cards: HuiCard[] = [];
|
||||||
@@ -58,6 +61,12 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
@state() _dragging = false;
|
@state() _dragging = false;
|
||||||
|
|
||||||
|
@state() private _showSidebar = false;
|
||||||
|
|
||||||
|
private _contentScrollTop = 0;
|
||||||
|
|
||||||
|
private _sidebarScrollTop = 0;
|
||||||
|
|
||||||
private _columnsController = new ResizeController(this, {
|
private _columnsController = new ResizeController(this, {
|
||||||
callback: (entries) => {
|
callback: (entries) => {
|
||||||
const totalWidth = entries[0]?.contentRect.width;
|
const totalWidth = entries[0]?.contentRect.width;
|
||||||
@@ -135,16 +144,31 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
const sections = this.sections;
|
const sections = this.sections;
|
||||||
const totalSectionCount =
|
const totalSectionCount =
|
||||||
this._sectionColumnCount + (this.lovelace?.editMode ? 1 : 0);
|
this._sectionColumnCount +
|
||||||
|
(this.lovelace?.editMode ? 1 : 0) +
|
||||||
|
(this._config?.sidebar ? 1 : 0);
|
||||||
const editMode = this.lovelace.editMode;
|
const editMode = this.lovelace.editMode;
|
||||||
|
|
||||||
const maxColumnCount = this._columnsController.value ?? 1;
|
const maxColumnCount = this._columnsController.value ?? 1;
|
||||||
|
|
||||||
|
const columnCount = Math.min(maxColumnCount, totalSectionCount);
|
||||||
|
// On mobile with sidebar, use full width for whichever view is active
|
||||||
|
const contentColumnCount =
|
||||||
|
this._config?.sidebar && !this.narrow
|
||||||
|
? Math.max(1, columnCount - 1)
|
||||||
|
: columnCount;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="wrapper ${classMap({
|
class="wrapper ${classMap({
|
||||||
"top-margin": Boolean(this._config?.top_margin),
|
"top-margin": Boolean(this._config?.top_margin),
|
||||||
|
"has-sidebar": Boolean(this._config?.sidebar),
|
||||||
|
narrow: this.narrow,
|
||||||
})}"
|
})}"
|
||||||
|
style=${styleMap({
|
||||||
|
"--column-count": columnCount,
|
||||||
|
"--content-column-count": contentColumnCount,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<hui-view-header
|
<hui-view-header
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -152,38 +176,54 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
.lovelace=${this.lovelace}
|
.lovelace=${this.lovelace}
|
||||||
.viewIndex=${this.index}
|
.viewIndex=${this.index}
|
||||||
.config=${this._config?.header}
|
.config=${this._config?.header}
|
||||||
style=${styleMap({
|
|
||||||
"--max-column-count": maxColumnCount,
|
|
||||||
})}
|
|
||||||
></hui-view-header>
|
></hui-view-header>
|
||||||
<ha-sortable
|
${this.narrow && this._config?.sidebar
|
||||||
.disabled=${!editMode}
|
? html`
|
||||||
@item-moved=${this._sectionMoved}
|
<div class="mobile-tabs">
|
||||||
group="section"
|
<ha-control-select
|
||||||
handle-selector=".handle"
|
.value=${this._showSidebar ? "sidebar" : "content"}
|
||||||
draggable-selector=".section"
|
@value-changed=${this._viewChanged}
|
||||||
.rollback=${false}
|
.options=${[
|
||||||
>
|
{
|
||||||
<div
|
value: "content",
|
||||||
class="container ${classMap({
|
label: this._config.sidebar.content_label,
|
||||||
dense: Boolean(this._config?.dense_section_placement),
|
},
|
||||||
})}"
|
{
|
||||||
style=${styleMap({
|
value: "sidebar",
|
||||||
"--total-section-count": totalSectionCount,
|
label: this._config.sidebar.sidebar_label,
|
||||||
"--max-column-count": maxColumnCount,
|
},
|
||||||
})}
|
]}
|
||||||
|
>
|
||||||
|
</ha-control-select>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<div class="container">
|
||||||
|
<ha-sortable
|
||||||
|
.disabled=${!editMode}
|
||||||
|
@item-moved=${this._sectionMoved}
|
||||||
|
group="section"
|
||||||
|
handle-selector=".handle"
|
||||||
|
draggable-selector=".section"
|
||||||
|
.rollback=${false}
|
||||||
>
|
>
|
||||||
${repeat(
|
<div
|
||||||
sections,
|
class="content ${classMap({
|
||||||
(section) => this._getSectionKey(section),
|
dense: Boolean(this._config?.dense_section_placement),
|
||||||
(section, idx) => {
|
"mobile-hidden": this.narrow && this._showSidebar,
|
||||||
const columnSpan = Math.min(
|
})}"
|
||||||
section.config.column_span || 1,
|
>
|
||||||
maxColumnCount
|
${repeat(
|
||||||
);
|
sections,
|
||||||
const rowSpan = section.config.row_span || 1;
|
(section) => this._getSectionKey(section),
|
||||||
|
(section, idx) => {
|
||||||
|
const columnSpan = Math.min(
|
||||||
|
section.config.column_span || 1,
|
||||||
|
contentColumnCount
|
||||||
|
);
|
||||||
|
const rowSpan = section.config.row_span || 1;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="section"
|
class="section"
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
@@ -208,72 +248,89 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
${editMode
|
${editMode
|
||||||
? html`
|
? html`
|
||||||
<ha-sortable
|
<ha-sortable
|
||||||
group="card"
|
group="card"
|
||||||
@item-added=${this._handleCardAdded}
|
@item-added=${this._handleCardAdded}
|
||||||
draggable-selector=".card"
|
draggable-selector=".card"
|
||||||
.rollback=${false}
|
.rollback=${false}
|
||||||
>
|
>
|
||||||
<div class="create-section-container">
|
<div class="create-section-container">
|
||||||
<div class="drop-helper" aria-hidden="true">
|
<div class="drop-helper" aria-hidden="true">
|
||||||
<p>
|
<p>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.lovelace.editor.section.drop_card_create_section"
|
"ui.panel.lovelace.editor.section.drop_card_create_section"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="create-section"
|
||||||
|
@click=${this._createSection}
|
||||||
|
aria-label=${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.section.create_section"
|
||||||
)}
|
)}
|
||||||
</p>
|
.title=${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.section.create_section"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ha-ripple></ha-ripple>
|
||||||
|
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
</ha-sortable>
|
||||||
class="create-section"
|
`
|
||||||
@click=${this._createSection}
|
: nothing}
|
||||||
aria-label=${this.hass.localize(
|
</div>
|
||||||
"ui.panel.lovelace.editor.section.create_section"
|
</ha-sortable>
|
||||||
)}
|
${this._config?.sidebar
|
||||||
.title=${this.hass.localize(
|
? html`
|
||||||
"ui.panel.lovelace.editor.section.create_section"
|
<hui-view-sidebar
|
||||||
)}
|
class=${classMap({
|
||||||
>
|
"mobile-hidden": this.narrow && !this._showSidebar,
|
||||||
<ha-ripple></ha-ripple>
|
})}
|
||||||
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
|
.hass=${this.hass}
|
||||||
</button>
|
.badges=${this.badges}
|
||||||
</div>
|
.lovelace=${this.lovelace}
|
||||||
</ha-sortable>
|
.viewIndex=${this.index}
|
||||||
`
|
.config=${this._config.sidebar}
|
||||||
: nothing}
|
></hui-view-sidebar>
|
||||||
${editMode && this._config?.cards?.length
|
`
|
||||||
? html`
|
: nothing}
|
||||||
<div class="section imported-cards">
|
</div>
|
||||||
<div class="imported-card-header">
|
<div class="imported-cards-section">
|
||||||
<p class="title">
|
${editMode && this._config?.cards?.length
|
||||||
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
|
? html`
|
||||||
${this.hass.localize(
|
<div class="section imported-cards">
|
||||||
"ui.panel.lovelace.editor.section.imported_cards_title"
|
<div class="imported-card-header">
|
||||||
)}
|
<p class="title">
|
||||||
</p>
|
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
|
||||||
<p class="subtitle">
|
${this.hass.localize(
|
||||||
${this.hass.localize(
|
"ui.panel.lovelace.editor.section.imported_cards_title"
|
||||||
"ui.panel.lovelace.editor.section.imported_cards_description"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<hui-section
|
|
||||||
.lovelace=${this.lovelace}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.config=${this._importedCardSectionConfig(
|
|
||||||
this._config.cards
|
|
||||||
)}
|
)}
|
||||||
.viewIndex=${this.index}
|
</p>
|
||||||
preview
|
<p class="subtitle">
|
||||||
import-only
|
${this.hass.localize(
|
||||||
></hui-section>
|
"ui.panel.lovelace.editor.section.imported_cards_description"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`
|
<hui-section
|
||||||
: nothing}
|
.lovelace=${this.lovelace}
|
||||||
</div>
|
.hass=${this.hass}
|
||||||
</ha-sortable>
|
.config=${this._importedCardSectionConfig(
|
||||||
|
this._config.cards
|
||||||
|
)}
|
||||||
|
.viewIndex=${this.index}
|
||||||
|
preview
|
||||||
|
import-only
|
||||||
|
></hui-section>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -352,6 +409,34 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
this.lovelace!.saveConfig(newConfig);
|
this.lovelace!.saveConfig(newConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _viewChanged(ev: CustomEvent) {
|
||||||
|
const newValue = ev.detail.value;
|
||||||
|
const shouldShowSidebar = newValue === "sidebar";
|
||||||
|
|
||||||
|
if (shouldShowSidebar !== this._showSidebar) {
|
||||||
|
this._toggleView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleView() {
|
||||||
|
// Save current scroll position
|
||||||
|
if (this._showSidebar) {
|
||||||
|
this._sidebarScrollTop = window.scrollY;
|
||||||
|
} else {
|
||||||
|
this._contentScrollTop = window.scrollY;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._showSidebar = !this._showSidebar;
|
||||||
|
|
||||||
|
// Restore scroll position after view updates
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
const scrollY = this._showSidebar
|
||||||
|
? this._sidebarScrollTop
|
||||||
|
: this._contentScrollTop;
|
||||||
|
window.scrollTo(0, scrollY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
--row-height: var(--ha-view-sections-row-height, 56px);
|
--row-height: var(--ha-view-sections-row-height, 56px);
|
||||||
@@ -369,14 +454,19 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper.top-margin {
|
.wrapper {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: var(--top-margin);
|
padding: var(--row-gap) var(--column-gap);
|
||||||
|
box-sizing: content-box;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: calc(
|
||||||
|
var(--column-count) * var(--column-max-width) +
|
||||||
|
(var(--column-count) - 1) * var(--column-gap)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container > * {
|
.wrapper.top-margin {
|
||||||
position: relative;
|
margin-top: var(--top-margin);
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
@@ -390,22 +480,92 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
--column-count: min(var(--max-column-count), var(--total-section-count));
|
display: grid;
|
||||||
|
grid-template-columns: [content-start] repeat(
|
||||||
|
var(--content-column-count),
|
||||||
|
1fr
|
||||||
|
);
|
||||||
|
gap: var(--row-gap) var(--column-gap);
|
||||||
|
padding: var(--row-gap) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper.has-sidebar .container {
|
||||||
|
grid-template-columns:
|
||||||
|
[content-start] repeat(var(--content-column-count), 1fr)
|
||||||
|
[sidebar-start] 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On mobile with sidebar, content and sidebar both take full width */
|
||||||
|
.wrapper.narrow.has-sidebar .container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
hui-view-sidebar {
|
||||||
|
grid-column: sidebar-start / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper.narrow hui-view-sidebar {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding-bottom: calc(
|
||||||
|
var(--ha-space-4) + 56px + var(--ha-space-4) +
|
||||||
|
env(safe-area-inset-bottom)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-tabs {
|
||||||
|
position: fixed;
|
||||||
|
bottom: calc(var(--ha-space-4) + env(safe-area-inset-bottom));
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 0;
|
||||||
|
z-index: 1;
|
||||||
|
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.15))
|
||||||
|
drop-shadow(0 4px 16px rgba(0, 0, 0, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-tabs ha-control-select {
|
||||||
|
width: max-content;
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: 90%;
|
||||||
|
--control-select-thickness: 56px;
|
||||||
|
--control-select-border-radius: var(--ha-border-radius-6xl);
|
||||||
|
--control-select-background: var(--card-background-color);
|
||||||
|
--control-select-background-opacity: 1;
|
||||||
|
--control-select-color: var(--primary-color);
|
||||||
|
--control-select-padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-sortable {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
grid-column: content-start / sidebar-start;
|
||||||
|
grid-row: 1 / -1;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
grid-template-columns: repeat(var(--column-count), 1fr);
|
grid-template-columns: repeat(var(--content-column-count), 1fr);
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
gap: var(--row-gap) var(--column-gap);
|
gap: var(--row-gap) var(--column-gap);
|
||||||
padding: var(--row-gap) var(--column-gap);
|
}
|
||||||
box-sizing: content-box;
|
|
||||||
margin: 0 auto;
|
.wrapper.narrow .content {
|
||||||
max-width: calc(
|
grid-column: 1 / -1;
|
||||||
var(--column-count) * var(--column-max-width) +
|
}
|
||||||
(var(--column-count) - 1) * var(--column-gap)
|
|
||||||
|
.wrapper.narrow.has-sidebar .content {
|
||||||
|
padding-bottom: calc(
|
||||||
|
var(--ha-space-4) + 56px + var(--ha-space-4) +
|
||||||
|
env(safe-area-inset-bottom)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.container.dense {
|
|
||||||
|
.content.dense {
|
||||||
grid-auto-flow: row dense;
|
grid-auto-flow: row dense;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,13 +643,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
hui-view-header {
|
hui-view-header {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0 var(--column-gap);
|
|
||||||
padding-top: var(--row-gap);
|
padding-top: var(--row-gap);
|
||||||
margin: auto;
|
|
||||||
max-width: calc(
|
|
||||||
var(--max-column-count) * var(--column-max-width) +
|
|
||||||
(var(--max-column-count) - 1) * var(--column-gap)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.imported-cards {
|
.imported-cards {
|
||||||
|
|||||||
57
src/panels/lovelace/views/hui-view-sidebar.ts
Normal file
57
src/panels/lovelace/views/hui-view-sidebar.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import type { LovelaceViewSidebarConfig } from "../../../data/lovelace/config/view";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import "../sections/hui-section";
|
||||||
|
import type { Lovelace } from "../types";
|
||||||
|
|
||||||
|
export const DEFAULT_VIEW_SIDEBAR_LAYOUT = "start";
|
||||||
|
|
||||||
|
@customElement("hui-view-sidebar")
|
||||||
|
export class HuiViewSidebar extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public config?: LovelaceViewSidebarConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public viewIndex!: number;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.lovelace) return nothing;
|
||||||
|
|
||||||
|
// Use preview mode instead of setting lovelace to avoid the sections to be
|
||||||
|
// editable as it is not yet supported
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
${repeat(
|
||||||
|
this.config?.sections || [],
|
||||||
|
(section) => html`
|
||||||
|
<hui-section
|
||||||
|
.config=${section}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.preview=${this.lovelace.editMode}
|
||||||
|
.viewIndex=${this.viewIndex}
|
||||||
|
></hui-section>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--row-gap, 8px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-view-sidebar": HuiViewSidebar;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7088,7 +7088,9 @@
|
|||||||
"unamed_device": "Unnamed device",
|
"unamed_device": "Unnamed device",
|
||||||
"others": "Others",
|
"others": "Others",
|
||||||
"scenes": "Scenes",
|
"scenes": "Scenes",
|
||||||
"automations": "Automations"
|
"automations": "Automations",
|
||||||
|
"for_you": "For you",
|
||||||
|
"home": "Home"
|
||||||
},
|
},
|
||||||
"common_controls": {
|
"common_controls": {
|
||||||
"not_loaded": "Usage Prediction integration is not loaded.",
|
"not_loaded": "Usage Prediction integration is not loaded.",
|
||||||
@@ -7360,6 +7362,8 @@
|
|||||||
"header": "View configuration",
|
"header": "View configuration",
|
||||||
"header_name": "{name} view configuration",
|
"header_name": "{name} view configuration",
|
||||||
"add": "Add view",
|
"add": "Add view",
|
||||||
|
"show_sidebar": "Show sidebar",
|
||||||
|
"show_content": "Show content",
|
||||||
"background": {
|
"background": {
|
||||||
"settings": "Background settings",
|
"settings": "Background settings",
|
||||||
"image": "Background image",
|
"image": "Background image",
|
||||||
|
|||||||
Reference in New Issue
Block a user