Compare commits

...

6 Commits

Author SHA1 Message Date
Paul Bottein
ed86f38ce2 Improve spacing 2025-11-24 16:50:28 +01:00
Paul Bottein
ba31ed0849 Use translations 2025-11-24 16:50:28 +01:00
Paul Bottein
5af9e0c9d4 Add mobile UI 2025-11-24 16:50:28 +01:00
Paul Bottein
8b49cea3ce Update home dashboard to use sidebar 2025-11-24 16:50:28 +01:00
Paul Bottein
6ab97da9bd Add sidebar to sections view 2025-11-24 16:50:27 +01:00
Bram Kragten
e65a8a6b66 Add device database toggle to analytics (#27948)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-24 16:30:26 +01:00
7 changed files with 499 additions and 206 deletions

View File

@@ -5,6 +5,7 @@ export interface AnalyticsPreferences {
diagnostics?: boolean;
usage?: boolean;
statistics?: boolean;
snapshots?: boolean;
}
export interface Analytics {

View File

@@ -1,7 +1,10 @@
import type { MediaSelectorValue } from "../../selector";
import type { LovelaceBadgeConfig } from "./badge";
import type { LovelaceCardConfig } from "./card";
import type { LovelaceSectionRawConfig } from "./section";
import type {
LovelaceSectionConfig,
LovelaceSectionRawConfig,
} from "./section";
import type { LovelaceStrategyConfig } from "./strategy";
export interface ShowViewConfig {
@@ -33,6 +36,12 @@ export interface LovelaceViewHeaderConfig {
badges_wrap?: "wrap" | "scroll";
}
export interface LovelaceViewSidebarConfig {
sections?: LovelaceSectionConfig[];
content_label?: string;
sidebar_label?: string;
}
export interface LovelaceBaseViewConfig {
index?: number;
title?: string;
@@ -56,6 +65,8 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
cards?: LovelaceCardConfig[];
sections?: LovelaceSectionRawConfig[];
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 {

View File

@@ -1,14 +1,10 @@
import { mdiOpenInNew } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-analytics";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-checkbox";
import "../../../components/ha-settings-row";
import "../../../components/ha-svg-icon";
import type { Analytics } from "../../../data/analytics";
import {
getAnalyticsDetails,
@@ -17,6 +13,8 @@ import {
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-alert";
@customElement("ha-config-analytics")
class ConfigAnalytics extends LitElement {
@@ -34,10 +32,22 @@ class ConfigAnalytics extends LitElement {
: undefined;
return html`
<ha-card outlined>
<ha-card
outlined
.header=${this.hass.localize("ui.panel.config.analytics.header") ||
"Home Assistant analytics"}
>
<div class="card-content">
${error ? html`<div class="error">${error}</div>` : ""}
<p>${this.hass.localize("ui.panel.config.analytics.intro")}</p>
${error ? html`<div class="error">${error}</div>` : nothing}
<p>
${this.hass.localize("ui.panel.config.analytics.intro")}
<a
href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>${this.hass.localize("ui.panel.config.analytics.learn_more")}</a
>.
</p>
<ha-analytics
translation_key_panel="config"
@analytics-preferences-changed=${this._preferencesChanged}
@@ -45,26 +55,59 @@ class ConfigAnalytics extends LitElement {
.analytics=${this._analyticsDetails}
></ha-analytics>
</div>
<div class="card-actions">
<ha-button @click=${this._save}>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.save_button"
)}
</ha-button>
</div>
</ha-card>
<div class="footer">
<ha-button
size="small"
appearance="plain"
href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
${this.hass.localize("ui.panel.config.analytics.learn_more")}
</ha-button>
</div>
${this._analyticsDetails &&
"snapshots" in this._analyticsDetails.preferences
? html`<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.header"
)}
>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.info"
)}
<a
href=${documentationUrl(this.hass, "/device-database/")}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.learn_more"
)}</a
>.
</p>
<ha-alert
.title=${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.alert.title"
)}
>${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.alert.content"
)}</ha-alert
>
<ha-settings-row>
<span slot="heading" data-for="snapshots">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.title`
)}
</span>
<span slot="description" data-for="snapshots">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.description`
)}
</span>
<ha-switch
@change=${this._handleDeviceRowClick}
.checked=${!!this._analyticsDetails?.preferences.snapshots}
.disabled=${this._analyticsDetails === undefined}
name="snapshots"
>
</ha-switch>
</ha-settings-row>
</div>
</ha-card>`
: nothing}
`;
}
@@ -96,11 +139,25 @@ class ConfigAnalytics extends LitElement {
}
}
private _handleDeviceRowClick(ev: Event) {
const target = ev.target as HaSwitch;
this._analyticsDetails = {
...this._analyticsDetails!,
preferences: {
...this._analyticsDetails!.preferences,
snapshots: target.checked,
},
};
this._save();
}
private _preferencesChanged(event: CustomEvent): void {
this._analyticsDetails = {
...this._analyticsDetails!,
preferences: event.detail.preferences,
};
this._save();
}
static get styles(): CSSResultGroup {
@@ -117,21 +174,10 @@ class ConfigAnalytics extends LitElement {
p {
margin-top: 0;
}
.card-actions {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: center;
ha-card:not(:first-of-type) {
margin-top: 24px;
}
.footer {
padding: 32px 0 16px;
text-align: center;
}
ha-button[size="small"] ha-svg-icon {
--mdc-icon-size: 16px;
}
`, // row-reverse so we tab first to "save"
`,
];
}
}

View File

@@ -1,5 +1,6 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import {
findEntities,
@@ -23,8 +24,8 @@ import type {
WeatherForecastCardConfig,
} from "../../cards/types";
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 type { Condition } from "../../common/validate-condition";
export interface HomeMainViewStrategyConfig {
type: "home-main";
@@ -70,8 +71,12 @@ export class HomeMainViewStrategy extends ReactiveElement {
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 = 2;
const maxColumns = 3;
const largeScreenCondition: Condition = {
condition: "screen",
media_query: "(min-width: 871px)",
};
const floorsSections: LovelaceSectionConfig[] = [];
for (const floorStructure of home.floors) {
@@ -126,12 +131,6 @@ export class HomeMainViewStrategy extends ReactiveElement {
});
}
const favoriteSection: LovelaceSectionConfig = {
type: "grid",
column_span: maxColumns,
cards: [],
};
const favoriteEntities = (config.favorite_entities || []).filter(
(entityId) => hass.states[entityId] !== undefined
);
@@ -176,74 +175,70 @@ export class HomeMainViewStrategy extends ReactiveElement {
({
type: "home-summary",
summary: "light",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/light?historyBack=1",
},
grid_options: {
rows: 2,
columns: 4,
columns: 12,
},
} satisfies HomeSummaryCard),
hasClimate &&
({
type: "home-summary",
summary: "climate",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/climate?historyBack=1",
},
grid_options: {
rows: 2,
columns: 4,
columns: 12,
},
} satisfies HomeSummaryCard),
hasSecurity &&
({
type: "home-summary",
summary: "security",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/security?historyBack=1",
},
grid_options: {
rows: 2,
columns: 4,
columns: 12,
},
} satisfies HomeSummaryCard),
hasMediaPlayers &&
({
type: "home-summary",
summary: "media_players",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "media-players",
},
grid_options: {
rows: 2,
columns: 4,
columns: 12,
},
} satisfies HomeSummaryCard),
].filter(Boolean) as LovelaceCardConfig[];
const summarySection: LovelaceSectionConfig = {
const forYouSection: LovelaceSectionConfig = {
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: [],
};
if (summaryCards.length) {
summarySection.cards!.push(
{
type: "heading",
heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"),
},
...summaryCards
);
widgetSection.cards!.push(...summaryCards);
}
const weatherFilter = generateEntityFilter(hass, {
@@ -251,28 +246,16 @@ export class HomeMainViewStrategy extends ReactiveElement {
entity_category: "none",
});
const widgetSection: LovelaceSectionConfig = {
type: "grid",
column_span: maxColumns,
cards: [],
};
const weatherEntity = Object.keys(hass.states)
.filter(weatherFilter)
.sort()[0];
if (weatherEntity) {
widgetSection.cards!.push(
{
type: "heading",
heading: "",
heading_style: "subtitle",
},
{
type: "weather-forecast",
entity: weatherEntity,
forecast_type: "daily",
} as WeatherForecastCardConfig
);
widgetSection.cards!.push({
type: "weather-forecast",
entity: weatherEntity,
forecast_type: "daily",
} as WeatherForecastCardConfig);
}
const energyPrefs = isComponentLoaded(hass, "energy")
@@ -299,11 +282,19 @@ export class HomeMainViewStrategy extends ReactiveElement {
const sections = (
[
favoriteSection.cards && favoriteSection,
{
type: "grid",
cards: [
// Heading to add some spacing on large screens
{
type: "heading",
heading_style: "subtitle",
visibility: [largeScreenCondition],
},
],
},
commonControlsSection,
summarySection.cards && summarySection,
...floorsSections,
widgetSection.cards && widgetSection,
] satisfies (LovelaceSectionRawConfig | undefined)[]
).filter(Boolean) as LovelaceSectionRawConfig[];
@@ -319,6 +310,11 @@ export class HomeMainViewStrategy extends ReactiveElement {
content: `## ${hass.localize("ui.panel.lovelace.strategy.home.welcome_user", { user: "{{ user }}" })}`,
} 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"),
},
};
}
}

View File

@@ -31,6 +31,7 @@ import {
import type { HuiSection } from "../sections/hui-section";
import type { Lovelace } from "../types";
import "./hui-view-header";
import "./hui-view-sidebar";
export const DEFAULT_MAX_COLUMNS = 4;
@@ -46,6 +47,8 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
@property({ attribute: false }) public isStrategy = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public sections: HuiSection[] = [];
@property({ attribute: false }) public cards: HuiCard[] = [];
@@ -58,6 +61,12 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
@state() _dragging = false;
@state() private _showSidebar = false;
private _contentScrollTop = 0;
private _sidebarScrollTop = 0;
private _columnsController = new ResizeController(this, {
callback: (entries) => {
const totalWidth = entries[0]?.contentRect.width;
@@ -135,16 +144,31 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
const sections = this.sections;
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 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`
<div
class="wrapper ${classMap({
"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
.hass=${this.hass}
@@ -152,38 +176,54 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
.lovelace=${this.lovelace}
.viewIndex=${this.index}
.config=${this._config?.header}
style=${styleMap({
"--max-column-count": maxColumnCount,
})}
></hui-view-header>
<ha-sortable
.disabled=${!editMode}
@item-moved=${this._sectionMoved}
group="section"
handle-selector=".handle"
draggable-selector=".section"
.rollback=${false}
>
<div
class="container ${classMap({
dense: Boolean(this._config?.dense_section_placement),
})}"
style=${styleMap({
"--total-section-count": totalSectionCount,
"--max-column-count": maxColumnCount,
})}
${this.narrow && this._config?.sidebar
? html`
<div class="mobile-tabs">
<ha-control-select
.value=${this._showSidebar ? "sidebar" : "content"}
@value-changed=${this._viewChanged}
.options=${[
{
value: "content",
label: this._config.sidebar.content_label,
},
{
value: "sidebar",
label: this._config.sidebar.sidebar_label,
},
]}
>
</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(
sections,
(section) => this._getSectionKey(section),
(section, idx) => {
const columnSpan = Math.min(
section.config.column_span || 1,
maxColumnCount
);
const rowSpan = section.config.row_span || 1;
<div
class="content ${classMap({
dense: Boolean(this._config?.dense_section_placement),
"mobile-hidden": this.narrow && this._showSidebar,
})}"
>
${repeat(
sections,
(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
class="section"
style=${styleMap({
@@ -208,72 +248,89 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
</div>
</div>
`;
}
)}
${editMode
? html`
<ha-sortable
group="card"
@item-added=${this._handleCardAdded}
draggable-selector=".card"
.rollback=${false}
>
<div class="create-section-container">
<div class="drop-helper" aria-hidden="true">
<p>
${this.hass.localize(
"ui.panel.lovelace.editor.section.drop_card_create_section"
}
)}
${editMode
? html`
<ha-sortable
group="card"
@item-added=${this._handleCardAdded}
draggable-selector=".card"
.rollback=${false}
>
<div class="create-section-container">
<div class="drop-helper" aria-hidden="true">
<p>
${this.hass.localize(
"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>
<button
class="create-section"
@click=${this._createSection}
aria-label=${this.hass.localize(
"ui.panel.lovelace.editor.section.create_section"
)}
.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>
</ha-sortable>
`
: nothing}
${editMode && this._config?.cards?.length
? html`
<div class="section imported-cards">
<div class="imported-card-header">
<p class="title">
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_title"
)}
</p>
<p class="subtitle">
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_description"
)}
</p>
</div>
<hui-section
.lovelace=${this.lovelace}
.hass=${this.hass}
.config=${this._importedCardSectionConfig(
this._config.cards
</ha-sortable>
`
: nothing}
</div>
</ha-sortable>
${this._config?.sidebar
? html`
<hui-view-sidebar
class=${classMap({
"mobile-hidden": this.narrow && !this._showSidebar,
})}
.hass=${this.hass}
.badges=${this.badges}
.lovelace=${this.lovelace}
.viewIndex=${this.index}
.config=${this._config.sidebar}
></hui-view-sidebar>
`
: nothing}
</div>
<div class="imported-cards-section">
${editMode && this._config?.cards?.length
? html`
<div class="section imported-cards">
<div class="imported-card-header">
<p class="title">
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_title"
)}
.viewIndex=${this.index}
preview
import-only
></hui-section>
</p>
<p class="subtitle">
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_description"
)}
</p>
</div>
`
: nothing}
</div>
</ha-sortable>
<hui-section
.lovelace=${this.lovelace}
.hass=${this.hass}
.config=${this._importedCardSectionConfig(
this._config.cards
)}
.viewIndex=${this.index}
preview
import-only
></hui-section>
</div>
`
: nothing}
</div>
</div>
`;
}
@@ -352,6 +409,46 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
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) {
// Currently showing sidebar, save its scroll position
const sidebar = this.shadowRoot?.querySelector("hui-view-sidebar");
if (sidebar) {
this._sidebarScrollTop = sidebar.scrollTop || 0;
}
} else {
// Currently showing content, save window scroll position
this._contentScrollTop = window.scrollY;
}
// Toggle view
this._showSidebar = !this._showSidebar;
// Restore scroll position after view updates
this.updateComplete.then(() => {
if (this._showSidebar) {
// Switched to sidebar, restore sidebar scroll
const sidebar = this.shadowRoot?.querySelector("hui-view-sidebar");
if (sidebar) {
sidebar.scrollTop = this._sidebarScrollTop;
}
} else {
// Switched to content, restore window scroll
window.scrollTo(0, this._contentScrollTop);
}
});
}
static styles = css`
:host {
--row-height: var(--ha-view-sections-row-height, 56px);
@@ -369,14 +466,19 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
}
}
.wrapper.top-margin {
.wrapper {
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 > * {
position: relative;
width: 100%;
.wrapper.top-margin {
margin-top: var(--top-margin);
}
.section {
@@ -390,22 +492,92 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
}
.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;
align-items: start;
justify-content: center;
grid-template-columns: repeat(var(--column-count), 1fr);
grid-template-columns: repeat(var(--content-column-count), 1fr);
grid-auto-flow: row;
gap: var(--row-gap) var(--column-gap);
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)
}
.wrapper.narrow .content {
grid-column: 1 / -1;
}
.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;
}
@@ -483,13 +655,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
hui-view-header {
display: block;
padding: 0 var(--column-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 {

View 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;
}
}

View File

@@ -6780,6 +6780,7 @@
},
"analytics": {
"caption": "Analytics",
"header": "Home Assistant analytics",
"description": "Learn how to share data to improve Home Assistant",
"preferences": {
"base": {
@@ -6797,10 +6798,21 @@
"diagnostics": {
"title": "Diagnostics",
"description": "Share crash reports when unexpected errors occur."
},
"snapshots": {
"title": "Devices",
"description": "Generic information about your devices.",
"header": "Device analytics",
"info": "Anonymously share data about your devices to help build the Open Home Foundations device database. This free, open source resource helps users find useful information about smart home devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).",
"learn_more": "Learn more about the device database and how we process your data",
"alert": {
"title": "Important",
"content": "Only enable this option if you understand that your device information will be shared."
}
}
},
"need_base_enabled": "You need to enable basic analytics for this option to be available",
"learn_more": "How we process your data",
"learn_more": "Learn how we process your data",
"intro": "Share anonymized information from your installation to help make Home Assistant better and help us convince manufacturers to add local control and privacy-focused features.",
"download_device_info": "Preview device analytics"
},
@@ -7059,7 +7071,9 @@
"unamed_device": "Unnamed device",
"others": "Others",
"scenes": "Scenes",
"automations": "Automations"
"automations": "Automations",
"for_you": "For you",
"home": "Home"
},
"common_controls": {
"not_loaded": "Usage Prediction integration is not loaded.",
@@ -7329,6 +7343,8 @@
"header": "View configuration",
"header_name": "{name} view configuration",
"add": "Add view",
"show_sidebar": "Show sidebar",
"show_content": "Show content",
"background": {
"settings": "Background settings",
"image": "Background image",