Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b621042d4e | ||
|
|
ebc35db562 |
No files matched your search
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
|
||||
<rect x="8" y="8" width="78" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="8" y="20" width="78" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="8" y="32" width="78" height="16" rx="4" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 519 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="#1C1C1C"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
|
||||
<rect x="8" y="8" width="78" height="8" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="8" y="20" width="78" height="8" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="8" y="32" width="78" height="16" rx="4" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 521 B |
@@ -0,0 +1,9 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="8" y="24" width="78" height="10" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="23.5" y="37.5" width="47" height="13" rx="6.5" fill="white" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="26" y="40" width="20" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="48" y="40" width="20" height="8" rx="4" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 713 B |
@@ -0,0 +1,9 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="#1C1C1C"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="8" y="24" width="78" height="10" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="23.5" y="37.5" width="47" height="13" rx="6.5" fill="#1C1C1C" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="26" y="40" width="20" height="8" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="48" y="40" width="20" height="8" rx="4" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 717 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
|
||||
<rect x="8" y="8" width="78" height="16" rx="4" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="8" y="28" width="78" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="8" y="40" width="78" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 519 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="#1C1C1C"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
|
||||
<rect x="8" y="8" width="78" height="16" rx="4" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="8" y="28" width="78" height="8" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="8" y="40" width="78" height="8" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 521 B |
@@ -0,0 +1,9 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
|
||||
<rect x="8" y="8" width="22" height="20" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="8" y="32" width="22" height="16" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="34" y="8" width="22" height="12" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="34" y="24" width="22" height="24" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="60" y="8" width="26" height="28" rx="4" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 690 B |
@@ -0,0 +1,9 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="#1C1C1C"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
|
||||
<rect x="8" y="8" width="22" height="20" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="8" y="32" width="22" height="16" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="34" y="8" width="22" height="12" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="34" y="24" width="22" height="24" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="60" y="8" width="26" height="28" rx="4" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 692 B |
@@ -0,0 +1,9 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
|
||||
<rect x="8" y="8" width="26" height="28" rx="4" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="38" y="8" width="22" height="20" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="38" y="32" width="22" height="16" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="64" y="8" width="22" height="12" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="64" y="24" width="22" height="24" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 691 B |
@@ -0,0 +1,9 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="#1C1C1C"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
|
||||
<rect x="8" y="8" width="26" height="28" rx="4" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="38" y="8" width="22" height="20" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="38" y="32" width="22" height="16" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="64" y="8" width="22" height="12" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="64" y="24" width="22" height="24" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 693 B |
@@ -49,6 +49,15 @@ export const normalizeViewConfig = (
|
||||
.map(normalizeSectionConfig);
|
||||
}
|
||||
|
||||
if (updatedView.sidebar?.sections) {
|
||||
updatedView.sidebar = {
|
||||
...updatedView.sidebar,
|
||||
sections: updatedView.sidebar.sections
|
||||
.filter(Boolean)
|
||||
.map(normalizeSectionConfig),
|
||||
};
|
||||
}
|
||||
|
||||
return updatedView;
|
||||
};
|
||||
|
||||
|
||||
@@ -48,8 +48,22 @@ export interface LovelaceViewFooterConfig {
|
||||
max_width?: number;
|
||||
}
|
||||
|
||||
export const SIDEBAR_POSITIONS = ["start", "end"] as const;
|
||||
|
||||
export type SidebarPosition = (typeof SIDEBAR_POSITIONS)[number];
|
||||
|
||||
export const DEFAULT_SIDEBAR_POSITION: SidebarPosition = "end";
|
||||
|
||||
export const SIDEBAR_MOBILE_POSITIONS = ["tabs", "top", "bottom"] as const;
|
||||
|
||||
export type SidebarMobilePosition = (typeof SIDEBAR_MOBILE_POSITIONS)[number];
|
||||
|
||||
export const DEFAULT_SIDEBAR_MOBILE_POSITION: SidebarMobilePosition = "bottom";
|
||||
|
||||
export interface LovelaceViewSidebarConfig {
|
||||
sections?: LovelaceSectionConfig[];
|
||||
position?: SidebarPosition;
|
||||
mobile_position?: SidebarMobilePosition;
|
||||
content_label?: string;
|
||||
sidebar_label?: string;
|
||||
visibility?: VisibilityCondition[];
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { mdiDelete, mdiDotsVertical, mdiPencil } from "@mdi/js";
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceViewSidebarConfig } from "../../../data/lovelace/config/view";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { deleteAtPath, getAtPath } from "../editor/lovelace-path";
|
||||
import { showEditSidebarDialog } from "../editor/sidebar-editor/show-edit-sidebar-dialog";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
@customElement("hui-sidebar-edit-header")
|
||||
export class HuiSidebarEditHeader extends LitElement {
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
|
||||
@property({ attribute: false }) public path!: LovelacePath;
|
||||
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="actions">
|
||||
<span class="title">
|
||||
${this._localize("ui.panel.lovelace.editor.sidebar.title")}
|
||||
</span>
|
||||
<ha-dropdown
|
||||
placement="bottom-end"
|
||||
@wa-select=${this._handleDropdownSelect}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this._localize("ui.panel.lovelace.editor.sidebar.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="edit">
|
||||
<ha-svg-icon slot="icon" .path=${mdiPencil}></ha-svg-icon>
|
||||
${this._localize("ui.common.edit")}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item value="delete" variant="danger">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this._localize("ui.common.delete")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: HaDropdownSelectEvent): void {
|
||||
const action = ev.detail?.item?.value;
|
||||
switch (action) {
|
||||
case "edit":
|
||||
showEditSidebarDialog(this, {
|
||||
lovelace: this.lovelace,
|
||||
path: this.path,
|
||||
});
|
||||
break;
|
||||
case "delete":
|
||||
this._deleteSidebar();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteSidebar() {
|
||||
const sidebar = getAtPath<LovelaceViewSidebarConfig>(
|
||||
this.lovelace.config,
|
||||
this.path
|
||||
);
|
||||
if (sidebar?.sections?.length) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this._localize("ui.panel.lovelace.editor.sidebar.delete_title"),
|
||||
text: this._localize("ui.panel.lovelace.editor.sidebar.delete_text"),
|
||||
confirmText: this._localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
});
|
||||
if (!confirm) return;
|
||||
}
|
||||
this.lovelace.saveConfig(deleteAtPath(this.lovelace.config, this.path));
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 34px;
|
||||
}
|
||||
.actions {
|
||||
position: absolute;
|
||||
height: 36px;
|
||||
bottom: -2px;
|
||||
inset-inline-end: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline-start: var(--ha-space-3);
|
||||
border-radius: var(
|
||||
--ha-section-border-radius,
|
||||
var(--ha-border-radius-xl)
|
||||
);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background: var(--secondary-background-color);
|
||||
color: var(--primary-text-color);
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-sidebar-edit-header": HuiSidebarEditHeader;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,10 @@ import type { LovelaceSectionElement } from "../../../data/lovelace";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { HuiErrorCard } from "../cards/hui-error-card";
|
||||
import "../sections/hui-grid-section";
|
||||
import "../sections/hui-stack-section";
|
||||
import { createLovelaceElement } from "./create-element-base";
|
||||
|
||||
const ALWAYS_LOADED_LAYOUTS = new Set(["grid"]);
|
||||
const ALWAYS_LOADED_LAYOUTS = new Set(["grid", "stack"]);
|
||||
|
||||
const LAZY_LOAD_LAYOUTS = {};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getParentPath,
|
||||
insertAtPath,
|
||||
getPathTarget,
|
||||
isViewSectionPath,
|
||||
moveAtPath,
|
||||
pathEquals,
|
||||
setAtPath,
|
||||
@@ -102,7 +103,15 @@ export const getCardSectionConfig = (
|
||||
if (!section || isStrategySection(section)) {
|
||||
return undefined;
|
||||
}
|
||||
return section;
|
||||
if (isViewSectionPath(containerPath)) {
|
||||
return section;
|
||||
}
|
||||
// Nested sections take the width of the section placed in the view
|
||||
const viewSectionPath = containerPath.slice(0, 4);
|
||||
const viewSection = isViewSectionPath(viewSectionPath)
|
||||
? getAtPath<LovelaceSectionRawConfig>(config, viewSectionPath)
|
||||
: undefined;
|
||||
return { ...section, column_span: viewSection?.column_span };
|
||||
};
|
||||
|
||||
export const addView = (
|
||||
|
||||
@@ -40,6 +40,10 @@ export const getParentPath = (path: LovelacePath): LovelacePath =>
|
||||
export const getViewPath = (path: LovelacePath): LovelacePath =>
|
||||
path.slice(0, 2);
|
||||
|
||||
export const isViewSectionPath = (path: LovelacePath): boolean =>
|
||||
path[path.length - 2] === "sections" &&
|
||||
pathEquals(getParentPath(getParentPath(path)), getViewPath(path));
|
||||
|
||||
export const getPathTarget = (path: LovelacePath): LovelacePathTarget => {
|
||||
const last = path[path.length - 1];
|
||||
if (typeof last === "number") {
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
getAtPath,
|
||||
getViewPath,
|
||||
insertAtPath,
|
||||
isViewSectionPath,
|
||||
moveAtPath,
|
||||
setAtPath,
|
||||
} from "../lovelace-path";
|
||||
@@ -224,6 +225,7 @@ export class HuiDialogEditSection
|
||||
<hui-section-settings-editor
|
||||
.config=${this._config}
|
||||
.viewConfig=${this._viewConfig}
|
||||
.nested=${!isViewSectionPath(this._params.path)}
|
||||
@value-changed=${this._configChanged}
|
||||
>
|
||||
</hui-section-settings-editor>
|
||||
|
||||
@@ -99,6 +99,8 @@ export class HuiDialogEditSection extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public viewConfig!: LovelaceViewConfig;
|
||||
|
||||
@property({ attribute: false }) public nested = false;
|
||||
|
||||
private get _maxColumns() {
|
||||
return this.viewConfig.max_columns || DEFAULT_MAX_COLUMNS;
|
||||
}
|
||||
@@ -120,9 +122,9 @@ export class HuiDialogEditSection extends LitElement {
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, maxColumns: number) =>
|
||||
(localize: LocalizeFunc, maxColumns: number, nested: boolean) =>
|
||||
[
|
||||
...computeColumnSpanSchemas(maxColumns, localize),
|
||||
...(nested ? [] : computeColumnSpanSchemas(maxColumns, localize)),
|
||||
{
|
||||
name: "background_enabled",
|
||||
selector: { boolean: {} },
|
||||
@@ -189,7 +191,7 @@ export class HuiDialogEditSection extends LitElement {
|
||||
theme: this.config.theme,
|
||||
};
|
||||
|
||||
const schema = this._schema(this._localize, this._maxColumns);
|
||||
const schema = this._schema(this._localize, this._maxColumns, this.nested);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../../../common/util/compute_rtl";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-tab-group";
|
||||
import "../../../../components/ha-tab-group-tab";
|
||||
import {
|
||||
DEFAULT_SIDEBAR_MOBILE_POSITION,
|
||||
DEFAULT_SIDEBAR_POSITION,
|
||||
SIDEBAR_MOBILE_POSITIONS,
|
||||
SIDEBAR_POSITIONS,
|
||||
type LovelaceViewSidebarConfig,
|
||||
type SidebarPosition,
|
||||
} from "../../../../data/lovelace/config/view";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import { getAtPath, setAtPath } from "../lovelace-path";
|
||||
import "../section-editor/hui-section-visibility-editor";
|
||||
import type { EditSidebarDialogParams } from "./show-edit-sidebar-dialog";
|
||||
|
||||
const TABS = ["settings", "visibility"] as const;
|
||||
|
||||
type Tab = (typeof TABS)[number];
|
||||
|
||||
const computeSide = (position: SidebarPosition, isRTL: boolean) => {
|
||||
if (position === "start") {
|
||||
return isRTL ? "right" : "left";
|
||||
}
|
||||
return isRTL ? "left" : "right";
|
||||
};
|
||||
|
||||
@customElement("hui-dialog-edit-sidebar")
|
||||
export class HuiDialogEditSidebar
|
||||
extends DirtyStateProviderMixin<LovelaceViewSidebarConfig>()(LitElement)
|
||||
implements HassDialog<EditSidebarDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: EditSidebarDialogParams;
|
||||
|
||||
@state() private _config?: LovelaceViewSidebarConfig;
|
||||
|
||||
@state() private _tab: Tab = "settings";
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
public showDialog(params: EditSidebarDialogParams): void {
|
||||
this._params = params;
|
||||
this._config =
|
||||
getAtPath<LovelaceViewSidebarConfig>(
|
||||
params.lovelace.config,
|
||||
params.path
|
||||
) ?? {};
|
||||
this._initDirtyTracking({ type: "deep" }, this._config);
|
||||
this._tab = "settings";
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this._config = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, isRTL: boolean) =>
|
||||
[
|
||||
{
|
||||
name: "position",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
options: SIDEBAR_POSITIONS.map((position) => ({
|
||||
value: position,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.sidebar.position_options.${computeSide(position, isRTL)}`
|
||||
),
|
||||
image: {
|
||||
src: `/static/images/form/sidebar_position_${position}.svg`,
|
||||
src_dark: `/static/images/form/sidebar_position_${position}_dark.svg`,
|
||||
flip_rtl: true,
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mobile_position",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
options: SIDEBAR_MOBILE_POSITIONS.map((position) => ({
|
||||
value: position,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.sidebar.mobile_position_options.${position}`
|
||||
),
|
||||
image: {
|
||||
src: `/static/images/form/sidebar_mobile_position_${position}.svg`,
|
||||
src_dark: `/static/images/form/sidebar_mobile_position_${position}_dark.svg`,
|
||||
flip_rtl: true,
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "content_label",
|
||||
visible: { field: "mobile_position", value: "tabs" },
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "sidebar_label",
|
||||
visible: { field: "mobile_position", value: "tabs" },
|
||||
selector: { text: {} },
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const data = {
|
||||
...this._config,
|
||||
position: this._config.position ?? DEFAULT_SIDEBAR_POSITION,
|
||||
mobile_position:
|
||||
this._config.mobile_position ?? DEFAULT_SIDEBAR_MOBILE_POSITION,
|
||||
};
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.sidebar.settings"
|
||||
)}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-tab-group @wa-tab-show=${this._tabChanged}>
|
||||
${TABS.map(
|
||||
(tab) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${tab}
|
||||
.active=${this._tab === tab}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.sidebar.tab_${tab}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
</ha-tab-group>
|
||||
${
|
||||
this._tab === "settings"
|
||||
? html`
|
||||
<ha-form
|
||||
autofocus
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._schema(this.hass.localize, isRTL)}
|
||||
.computeLabel=${this._computeLabel}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`
|
||||
: html`
|
||||
<hui-section-visibility-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-section-visibility-editor>
|
||||
`
|
||||
}
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${this._saving || !this.isDirtyState}
|
||||
.loading=${this._saving}
|
||||
@click=${this._save}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => this.hass.localize(`ui.panel.lovelace.editor.sidebar.${schema.name}`);
|
||||
|
||||
private _tabChanged(ev: CustomEvent<{ name: Tab }>) {
|
||||
this._tab = ev.detail.name;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<LovelaceViewSidebarConfig>) {
|
||||
ev.stopPropagation();
|
||||
this._config = ev.detail.value;
|
||||
this._updateDirtyState(this._config);
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
if (!this._params || !this._config) {
|
||||
return;
|
||||
}
|
||||
const { lovelace, path } = this._params;
|
||||
this._saving = true;
|
||||
try {
|
||||
await lovelace.saveConfig(setAtPath(lovelace.config, path, this._config));
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
text: `${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.sidebar.saving_failed"
|
||||
)}: ${err.message}`,
|
||||
});
|
||||
} finally {
|
||||
this._saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-tab-group {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
ha-tab-group-tab {
|
||||
flex: 1;
|
||||
}
|
||||
ha-tab-group-tab::part(base) {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-edit-sidebar": HuiDialogEditSidebar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { Lovelace } from "../../types";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
|
||||
export interface EditSidebarDialogParams {
|
||||
lovelace: Lovelace;
|
||||
path: LovelacePath;
|
||||
}
|
||||
|
||||
export const showEditSidebarDialog = (
|
||||
element: HTMLElement,
|
||||
params: EditSidebarDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-edit-sidebar",
|
||||
dialogImport: () => import("./hui-dialog-edit-sidebar"),
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
mdiFormatListBulletedTriangle,
|
||||
mdiHelpCircleOutline,
|
||||
mdiMagnify,
|
||||
mdiPageLayoutSidebarRight,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiRedo,
|
||||
@@ -58,6 +59,7 @@ import type {
|
||||
} from "../../data/lovelace/config/types";
|
||||
import { isStrategyDashboard } from "../../data/lovelace/config/types";
|
||||
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
|
||||
import { isStrategyView } from "../../data/lovelace/config/view";
|
||||
import {
|
||||
deleteDashboard,
|
||||
fetchDashboards,
|
||||
@@ -87,6 +89,7 @@ import { showAddIntegrationDialog } from "../config/integrations/show-add-integr
|
||||
import { showDashboardDetailDialog } from "../config/lovelace/dashboards/show-dialog-lovelace-dashboard-detail";
|
||||
import { showPersonDetailDialog } from "../config/person/show-dialog-person-detail";
|
||||
import { swapView } from "./editor/config-util";
|
||||
import { setAtPath } from "./editor/lovelace-path";
|
||||
import { showDashboardStrategyEditorDialog } from "./editor/dashboard-strategy-editor/dialogs/show-dialog-dashboard-strategy-editor";
|
||||
import { showSaveDialog } from "./editor/show-save-config-dialog";
|
||||
import { showEditViewDialog } from "./editor/view-editor/show-edit-view-dialog";
|
||||
@@ -97,6 +100,8 @@ import "./views/hui-view";
|
||||
import type { HUIView } from "./views/hui-view";
|
||||
import "./views/hui-view-background";
|
||||
import "./views/hui-view-container";
|
||||
import { SECTIONS_VIEW_LAYOUT } from "./views/const";
|
||||
import { getViewType } from "./views/get-view-type";
|
||||
import { LOVELACE_EDIT_TOAST_ID } from "./types";
|
||||
|
||||
interface ActionItem {
|
||||
@@ -219,7 +224,24 @@ class HUIRoot extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
const isLovelaceDashboard = this.panel?.component_name === "lovelace";
|
||||
|
||||
const curViewConfig =
|
||||
typeof this._curView === "number"
|
||||
? this.lovelace?.config.views[this._curView]
|
||||
: undefined;
|
||||
const canAddSidebar =
|
||||
curViewConfig !== undefined &&
|
||||
!isStrategyView(curViewConfig) &&
|
||||
getViewType(curViewConfig) === SECTIONS_VIEW_LAYOUT &&
|
||||
!curViewConfig.sidebar;
|
||||
|
||||
const items: ActionItem[] = [
|
||||
{
|
||||
icon: mdiPageLayoutSidebarRight,
|
||||
key: "ui.panel.lovelace.editor.menu.add_sidebar",
|
||||
overflowAction: this._handleAddSidebar,
|
||||
visible: this._editMode && canAddSidebar,
|
||||
overflow: true,
|
||||
},
|
||||
{
|
||||
icon: mdiFormatListBulletedTriangle,
|
||||
key: "ui.panel.lovelace.unused_entities.title",
|
||||
@@ -972,6 +994,13 @@ class HUIRoot extends KeyboardShortcutMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private _handleAddSidebar = () => {
|
||||
const sidebarPath = ["views", this._curView as number, "sidebar"];
|
||||
this.lovelace!.saveConfig(
|
||||
setAtPath(this.lovelace!.config, sidebarPath, { sections: [] })
|
||||
);
|
||||
};
|
||||
|
||||
private _handleRawEditor = () => {
|
||||
this.lovelace!.enableFullEditMode();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import { mdiViewGridPlus } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceSectionElement } from "../../../data/lovelace";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import { isStrategySection } from "../../../data/lovelace/config/section";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/hui-section-edit-mode";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { moveAtPath } from "../editor/lovelace-path";
|
||||
import { showCreateSectionDialog } from "../editor/section-editor/show-create-section-dialog";
|
||||
import type { Lovelace } from "../types";
|
||||
import type { HuiSection } from "./hui-section";
|
||||
import {
|
||||
renderSectionContainer,
|
||||
sectionContainerStyles,
|
||||
} from "./section-container";
|
||||
|
||||
@customElement("hui-stack-section")
|
||||
export class HuiStackSection
|
||||
extends LitElement
|
||||
implements LovelaceSectionElement
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ attribute: false }) public path?: LovelacePath;
|
||||
|
||||
@property({ attribute: false }) public isStrategy = false;
|
||||
|
||||
@property({ attribute: false }) public sections: HuiSection[] = [];
|
||||
|
||||
public setConfig(_config: LovelaceSectionConfig): void {
|
||||
// The stack has no settings of its own
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const { hass, lovelace, path } = this;
|
||||
if (!hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const editMode = Boolean(lovelace?.editMode && !this.isStrategy);
|
||||
if (!editMode || !lovelace || !path) {
|
||||
return html`
|
||||
<div class="sections">
|
||||
${repeat(
|
||||
this.sections,
|
||||
(section) => section,
|
||||
(section) =>
|
||||
html`<div class="section">
|
||||
${renderSectionContainer(hass, section.config, section)}
|
||||
</div>`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const sectionsPath = [...path, "sections"];
|
||||
|
||||
return html`
|
||||
<ha-sortable
|
||||
@item-moved=${this._sectionMoved}
|
||||
@item-added=${this._sectionAdded}
|
||||
@item-removed=${this._sectionRemoved}
|
||||
group="section"
|
||||
handle-selector=".handle"
|
||||
draggable-selector=".section"
|
||||
>
|
||||
<div class="sections">
|
||||
${repeat(
|
||||
this.sections,
|
||||
(section) => section,
|
||||
(section, idx) => html`
|
||||
<div class="section" .sortableData=${[...sectionsPath, idx]}>
|
||||
<hui-section-edit-mode
|
||||
.hass=${hass}
|
||||
.lovelace=${lovelace}
|
||||
.path=${[...sectionsPath, idx]}
|
||||
.isStrategy=${isStrategySection(section.config)}
|
||||
>
|
||||
${renderSectionContainer(hass, section.config, section)}
|
||||
</hui-section-edit-mode>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<button
|
||||
class="create-section"
|
||||
@click=${this._createSection}
|
||||
aria-label=${hass.localize(
|
||||
"ui.panel.lovelace.editor.section.create_section"
|
||||
)}
|
||||
.title=${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>
|
||||
`;
|
||||
}
|
||||
|
||||
private _sectionMoved(ev: HASSDomEvent<HASSDomEvents["item-moved"]>) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const sectionsPath = [...this.path!, "sections"];
|
||||
this.lovelace!.saveConfig(
|
||||
moveAtPath(
|
||||
this.lovelace!.config,
|
||||
[...sectionsPath, oldIndex],
|
||||
[...sectionsPath, newIndex]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _sectionAdded(ev: HASSDomEvent<HASSDomEvents["item-added"]>) {
|
||||
ev.stopPropagation();
|
||||
const { index, data } = ev.detail;
|
||||
this.lovelace!.saveConfig(
|
||||
moveAtPath(this.lovelace!.config, data as LovelacePath, [
|
||||
...this.path!,
|
||||
"sections",
|
||||
index,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
private _sectionRemoved(ev: HASSDomEvent<HASSDomEvents["item-removed"]>) {
|
||||
// The list the section is dropped in moves it
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _createSection() {
|
||||
showCreateSectionDialog(this, {
|
||||
lovelace: this.lovelace!,
|
||||
containerPath: this.path!,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = [
|
||||
sectionContainerStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--row-gap, 8px);
|
||||
}
|
||||
.section:has(hui-section[hidden]) {
|
||||
display: none;
|
||||
}
|
||||
.create-section {
|
||||
display: block;
|
||||
position: relative;
|
||||
outline: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(
|
||||
--ha-section-border-radius,
|
||||
var(--ha-border-radius-xl)
|
||||
);
|
||||
border: 2px dashed var(--primary-color);
|
||||
height: calc(var(--row-height, 56px) + 2 * (var(--row-gap, 8px) + 2px));
|
||||
padding: var(--ha-space-2);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
--ha-ripple-color: var(--primary-color);
|
||||
--ha-ripple-hover-opacity: 0.04;
|
||||
--ha-ripple-pressed-opacity: 0.12;
|
||||
}
|
||||
.create-section:focus {
|
||||
border: 2px solid var(--primary-color);
|
||||
}
|
||||
.sortable-ghost {
|
||||
border-radius: var(
|
||||
--ha-section-border-radius,
|
||||
var(--ha-border-radius-xl)
|
||||
);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-stack-section": HuiStackSection;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { clamp } from "../../../common/number/clamp";
|
||||
import { getHistoryState, updateHistoryState } from "../../../common/navigate";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -17,19 +18,26 @@ import "../../../components/ha-svg-icon";
|
||||
import { maxColumnsContext } from "../common/context";
|
||||
import type { LovelaceViewElement } from "../../../data/lovelace";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import {
|
||||
DEFAULT_SECTION_COLUMN_SPAN,
|
||||
isStrategySection,
|
||||
} from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import {
|
||||
DEFAULT_SIDEBAR_MOBILE_POSITION,
|
||||
type LovelaceViewConfig,
|
||||
type LovelaceViewSidebarConfig,
|
||||
} from "../../../data/lovelace/config/view";
|
||||
import { DEFAULT_MAX_COLUMNS } from "./const";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HuiBadge } from "../badges/hui-badge";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import "../components/hui-section-edit-mode";
|
||||
import "../components/hui-sidebar-edit-header";
|
||||
import { addSection } from "../editor/config-util";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { getAtPath, moveAtPath } from "../editor/lovelace-path";
|
||||
import "../sections/hui-section";
|
||||
import { showCreateSectionDialog } from "../editor/section-editor/show-create-section-dialog";
|
||||
import type { HuiSection } from "../sections/hui-section";
|
||||
import {
|
||||
@@ -40,7 +48,6 @@ import type { Lovelace } from "../types";
|
||||
import { generateDefaultSection } from "./default-section";
|
||||
import "./hui-view-footer";
|
||||
import "./hui-view-header";
|
||||
import "./hui-view-sidebar";
|
||||
import { computeSectionsBackgroundAlignment } from "./sections-background-alignment";
|
||||
|
||||
const parsePx = (value: string) => parseInt(value.replace("px", ""));
|
||||
@@ -181,18 +188,19 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
const sections = this.sections;
|
||||
const editMode = this.lovelace.editMode;
|
||||
const sidebar = this._config?.sidebar;
|
||||
const sidebarEditMode = editMode && !this.isStrategy;
|
||||
const hasSidebar =
|
||||
this._config?.sidebar && (this._sidebarVisible || editMode);
|
||||
sidebar &&
|
||||
(editMode || (this._sidebarVisible && Boolean(sidebar.sections?.length)));
|
||||
const singleColumn = this._maxColumns <= 1;
|
||||
|
||||
// The tab switcher is opt-in: a sidebar enables it by labelling both tabs,
|
||||
// otherwise the sidebar stacks below the content.
|
||||
const mobilePosition =
|
||||
this._config?.sidebar?.mobile_position ?? DEFAULT_SIDEBAR_MOBILE_POSITION;
|
||||
const useSidebarTabs = Boolean(
|
||||
singleColumn &&
|
||||
hasSidebar &&
|
||||
this._config?.sidebar?.content_label &&
|
||||
this._config?.sidebar?.sidebar_label
|
||||
singleColumn && hasSidebar && mobilePosition === "tabs"
|
||||
);
|
||||
const sidebarOnTop = singleColumn && mobilePosition === "top";
|
||||
|
||||
const totalSectionCount =
|
||||
this._sectionColumnCount + (editMode ? 1 : 0) + (hasSidebar ? 1 : 0);
|
||||
@@ -218,6 +226,8 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
"has-sidebar": Boolean(hasSidebar),
|
||||
"single-column": singleColumn,
|
||||
"has-sidebar-tabs": useSidebarTabs,
|
||||
"sidebar-top": sidebarOnTop,
|
||||
"sidebar-at-start": sidebar?.position === "start",
|
||||
})}"
|
||||
style=${styleMap({
|
||||
"--column-count": columnCount,
|
||||
@@ -241,11 +251,19 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
.options=${[
|
||||
{
|
||||
value: "content",
|
||||
label: this._config!.sidebar!.content_label,
|
||||
label:
|
||||
this._config!.sidebar!.content_label ||
|
||||
this.hass.localize(
|
||||
"ui.panel.lovelace.views.sidebar_tabs.content"
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "sidebar",
|
||||
label: this._config!.sidebar!.sidebar_label,
|
||||
label:
|
||||
this._config!.sidebar!.sidebar_label ||
|
||||
this.hass.localize(
|
||||
"ui.panel.lovelace.views.sidebar_tabs.sidebar"
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -258,6 +276,8 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
<ha-sortable
|
||||
.disabled=${!editMode}
|
||||
@item-moved=${this._sectionMoved}
|
||||
@item-added=${this._sectionAdded}
|
||||
@item-removed=${this._sectionRemoved}
|
||||
group="section"
|
||||
handle-selector=".handle"
|
||||
draggable-selector=".section"
|
||||
@@ -281,6 +301,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
return html`
|
||||
<div
|
||||
class="section"
|
||||
.sortableData=${[...this.path!, "sections", idx]}
|
||||
style=${styleMap({
|
||||
"--column-span": columnSpan,
|
||||
"--row-span": rowSpan,
|
||||
@@ -348,23 +369,41 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
</div>
|
||||
</ha-sortable>
|
||||
${
|
||||
this._config?.sidebar
|
||||
sidebar
|
||||
? html`
|
||||
<hui-view-sidebar
|
||||
<div
|
||||
class=${classMap({
|
||||
sidebar: true,
|
||||
"edit-mode": sidebarEditMode,
|
||||
hidden:
|
||||
!hasSidebar ||
|
||||
(useSidebarTabs && !this._sidebarTabActive),
|
||||
})}
|
||||
.hass=${this.hass}
|
||||
.badges=${this.badges}
|
||||
.lovelace=${this.lovelace}
|
||||
.path=${this.path!}
|
||||
.config=${this._config.sidebar}
|
||||
@sidebar-visibility-changed=${
|
||||
this._handleSidebarVisibilityChanged
|
||||
>
|
||||
${
|
||||
sidebarEditMode
|
||||
? html`
|
||||
<hui-sidebar-edit-header
|
||||
.lovelace=${this.lovelace}
|
||||
.path=${this._sidebarPath(this.path!)}
|
||||
></hui-sidebar-edit-header>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
></hui-view-sidebar>
|
||||
<div class="sidebar-content">
|
||||
<hui-section
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this.lovelace}
|
||||
.config=${this._sidebarSectionConfig(sidebar)}
|
||||
.path=${this._sidebarPath(this.path!)}
|
||||
.preview=${editMode}
|
||||
.insideStrategy=${this.isStrategy}
|
||||
@section-visibility-changed=${
|
||||
this._handleSidebarVisibilityChanged
|
||||
}
|
||||
></hui-section>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
@@ -481,6 +520,22 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
private _sectionAdded(ev: HASSDomEvent<HASSDomEvents["item-added"]>) {
|
||||
ev.stopPropagation();
|
||||
const { index, data } = ev.detail;
|
||||
const newConfig = moveAtPath(this.lovelace!.config, data as LovelacePath, [
|
||||
...this.path!,
|
||||
"sections",
|
||||
index,
|
||||
]);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
private _sectionRemoved(ev: HASSDomEvent<HASSDomEvents["item-removed"]>) {
|
||||
// The list the section is dropped in moves it
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _viewChanged(ev: CustomEvent) {
|
||||
const newValue = ev.detail.value;
|
||||
const shouldShowSidebar = newValue === "sidebar";
|
||||
@@ -512,12 +567,30 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
});
|
||||
}
|
||||
|
||||
// A stack section also hides when all its sections are hidden
|
||||
private _sidebarSectionConfig = memoizeOne(
|
||||
(sidebar: LovelaceViewSidebarConfig): LovelaceSectionConfig => ({
|
||||
type: "stack",
|
||||
sections: sidebar.sections,
|
||||
visibility: sidebar.visibility,
|
||||
})
|
||||
);
|
||||
|
||||
private _sidebarPath = memoizeOne((path: LovelacePath): LovelacePath => [
|
||||
...path,
|
||||
"sidebar",
|
||||
]);
|
||||
|
||||
private _handleSidebarVisibilityChanged = (
|
||||
e: CustomEvent<{ visible: boolean }>
|
||||
ev: CustomEvent<{ value: boolean }>
|
||||
) => {
|
||||
this._sidebarVisible = e.detail.visible;
|
||||
// Sections inside the sidebar fire the same event
|
||||
if (ev.target !== ev.currentTarget) {
|
||||
return;
|
||||
}
|
||||
this._sidebarVisible = ev.detail.value;
|
||||
// Reset sidebar tab when sidebar becomes hidden
|
||||
if (!e.detail.visible) {
|
||||
if (!ev.detail.value) {
|
||||
this._sidebarTabActive = false;
|
||||
}
|
||||
};
|
||||
@@ -575,10 +648,9 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: [content-start] repeat(
|
||||
var(--content-column-count),
|
||||
1fr
|
||||
);
|
||||
grid-template-columns:
|
||||
[content-start] repeat(var(--content-column-count), 1fr)
|
||||
[content-end];
|
||||
gap: var(--row-gap) var(--column-gap);
|
||||
padding: var(--row-gap) 0;
|
||||
align-items: flex-start;
|
||||
@@ -588,7 +660,13 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
.wrapper.has-sidebar .container {
|
||||
grid-template-columns:
|
||||
[content-start] repeat(var(--content-column-count), 1fr)
|
||||
[sidebar-start] 1fr;
|
||||
[content-end sidebar-start] 1fr [sidebar-end];
|
||||
}
|
||||
|
||||
.wrapper.has-sidebar.sidebar-at-start .container {
|
||||
grid-template-columns:
|
||||
[sidebar-start] 1fr [sidebar-end content-start]
|
||||
repeat(var(--content-column-count), 1fr) [content-end];
|
||||
}
|
||||
|
||||
/* With a single column, content and sidebar stack at full width */
|
||||
@@ -596,15 +674,33 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
hui-view-sidebar {
|
||||
grid-column: sidebar-start / -1;
|
||||
.sidebar {
|
||||
grid-column: sidebar-start / sidebar-end;
|
||||
}
|
||||
|
||||
.wrapper.single-column hui-view-sidebar {
|
||||
.sidebar hui-section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar.edit-mode .sidebar-content {
|
||||
padding: var(--ha-space-2);
|
||||
border: 2px dashed var(--divider-color);
|
||||
border-radius: var(
|
||||
--ha-section-border-radius,
|
||||
var(--ha-border-radius-xl)
|
||||
);
|
||||
border-start-end-radius: 0;
|
||||
}
|
||||
|
||||
.wrapper.sidebar-top .sidebar {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.wrapper.single-column .sidebar {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.wrapper.has-sidebar-tabs hui-view-sidebar {
|
||||
.wrapper.has-sidebar-tabs .sidebar {
|
||||
padding-bottom: calc(
|
||||
var(--ha-space-14) + var(--ha-space-3) + var(--safe-area-inset-bottom)
|
||||
);
|
||||
@@ -641,7 +737,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-column: content-start / sidebar-start;
|
||||
grid-column: content-start / content-end;
|
||||
grid-row: 1 / -1;
|
||||
display: grid;
|
||||
align-items: start;
|
||||
@@ -653,6 +749,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
.wrapper.single-column .content {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: auto;
|
||||
}
|
||||
|
||||
.wrapper.has-sidebar-tabs .content {
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { LovelaceViewSidebarConfig } from "../../../data/lovelace/config/view";
|
||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../sections/hui-section";
|
||||
import {
|
||||
renderSectionContainer,
|
||||
sectionContainerStyles,
|
||||
} from "../sections/section-container";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
|
||||
export const DEFAULT_VIEW_SIDEBAR_LAYOUT = "start";
|
||||
|
||||
@customElement("hui-view-sidebar")
|
||||
export class HuiViewSidebar extends ConditionalListenerMixin<LovelaceViewSidebarConfig>(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceViewSidebarConfig;
|
||||
|
||||
@property({ attribute: false }) public path!: LovelacePath;
|
||||
|
||||
private _visible = true;
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("hass") || changedProperties.has("config")) {
|
||||
this._updateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateVisibility(conditionsMet?: boolean) {
|
||||
if (!this.hass || !this.config) return;
|
||||
|
||||
const visible = conditionsMet ?? this._conditionsVisible();
|
||||
|
||||
if (visible !== this._visible) {
|
||||
this._visible = visible;
|
||||
fireEvent(this, "sidebar-visibility-changed", { visible });
|
||||
}
|
||||
}
|
||||
|
||||
private _sectionConfigKeys = new WeakMap<LovelaceSectionConfig, string>();
|
||||
|
||||
private _getSectionKey(section: LovelaceSectionConfig) {
|
||||
if (!this._sectionConfigKeys.has(section)) {
|
||||
this._sectionConfigKeys.set(section, Math.random().toString());
|
||||
}
|
||||
return this._sectionConfigKeys.get(section)!;
|
||||
}
|
||||
|
||||
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) => this._getSectionKey(section),
|
||||
(section, idx) => {
|
||||
const sectionPath = [...this.path, "sidebar", "sections", idx];
|
||||
const sectionElement = html`
|
||||
<hui-section
|
||||
.config=${section}
|
||||
.hass=${this.hass}
|
||||
.preview=${this.lovelace.editMode}
|
||||
.path=${sectionPath}
|
||||
></hui-section>
|
||||
`;
|
||||
return renderSectionContainer(this.hass, section, sectionElement);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
sectionContainerStyles,
|
||||
css`
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--row-gap, 8px);
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-view-sidebar": HuiViewSidebar;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"sidebar-visibility-changed": { visible: boolean };
|
||||
}
|
||||
}
|
||||
@@ -398,6 +398,7 @@ export class SecurityViewStrategy extends ReactiveElement {
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
mobile_position: "tabs",
|
||||
content_label: hass.localize(
|
||||
"ui.panel.lovelace.strategy.security.devices"
|
||||
),
|
||||
|
||||
@@ -10158,6 +10158,10 @@
|
||||
"saving_failed": "Saving dashboard configuration failed."
|
||||
},
|
||||
"views": {
|
||||
"sidebar_tabs": {
|
||||
"content": "Main",
|
||||
"sidebar": "Sidebar"
|
||||
},
|
||||
"unnamed_view": "Unnamed view",
|
||||
"delete_title": "Delete view",
|
||||
"delete_named_view_only": "''{name}'' view will be deleted.",
|
||||
@@ -10199,6 +10203,7 @@
|
||||
"undo_redo_failed_to_apply_changes": "Unable to apply changes: {error}",
|
||||
"menu": {
|
||||
"open": "Open dashboard menu",
|
||||
"add_sidebar": "Add sidebar",
|
||||
"raw_editor": "Raw configuration editor",
|
||||
"manage_dashboards": "Manage dashboards",
|
||||
"manage_resources": "Manage resources"
|
||||
@@ -10532,6 +10537,29 @@
|
||||
"built_in": "Built-in sections",
|
||||
"custom": "Community sections"
|
||||
},
|
||||
"sidebar": {
|
||||
"title": "Sidebar",
|
||||
"settings": "Sidebar settings",
|
||||
"tab_settings": "[%key:ui::panel::lovelace::editor::edit_view::tab_settings%]",
|
||||
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_view::tab_visibility%]",
|
||||
"menu": "Sidebar menu",
|
||||
"saving_failed": "[%key:ui::panel::lovelace::editor::edit_view::saving_failed%]",
|
||||
"position": "On large screens",
|
||||
"position_options": {
|
||||
"left": "Left",
|
||||
"right": "Right"
|
||||
},
|
||||
"mobile_position": "On small screens",
|
||||
"mobile_position_options": {
|
||||
"tabs": "In a separate tab",
|
||||
"top": "Above the content",
|
||||
"bottom": "Below the content"
|
||||
},
|
||||
"content_label": "Main tab name",
|
||||
"sidebar_label": "Sidebar tab name",
|
||||
"delete_title": "Delete sidebar",
|
||||
"delete_text": "The sidebar and all its sections will be deleted."
|
||||
},
|
||||
"edit_section": {
|
||||
"header": "Section configuration",
|
||||
"typed_header": "{type} section configuration",
|
||||
|
||||