Compare commits

...
Author SHA1 Message Date
Paul Bottein b621042d4e Add settings to the sections view sidebar 2026-10-27 09:58:32 +01:00
17 changed files with 465 additions and 14 deletions

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

+14
View File
@@ -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[];
@@ -1,4 +1,5 @@
import { mdiDelete, mdiDotsVertical } from "@mdi/js";
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";
@@ -12,6 +13,7 @@ import type { LovelaceViewSidebarConfig } from "../../../data/lovelace/config/vi
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")
@@ -38,6 +40,11 @@ export class HuiSidebarEditHeader extends LitElement {
.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")}
@@ -49,8 +56,16 @@ export class HuiSidebarEditHeader extends LitElement {
private _handleDropdownSelect(ev: HaDropdownSelectEvent): void {
const action = ev.detail?.item?.value;
if (action === "delete") {
this._deleteSidebar();
switch (action) {
case "edit":
showEditSidebarDialog(this, {
lovelace: this.lovelace,
path: this.path,
});
break;
case "delete":
this._deleteSidebar();
break;
}
}
@@ -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,
});
};
+31 -11
View File
@@ -23,9 +23,10 @@ import {
DEFAULT_SECTION_COLUMN_SPAN,
isStrategySection,
} from "../../../data/lovelace/config/section";
import type {
LovelaceViewConfig,
LovelaceViewSidebarConfig,
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";
@@ -194,14 +195,12 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
(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);
@@ -227,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,
@@ -250,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"
),
},
]}
>
@@ -654,6 +663,12 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
[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 */
.wrapper.single-column.has-sidebar .container {
grid-template-columns: 1fr;
@@ -677,6 +692,10 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
border-start-end-radius: 0;
}
.wrapper.sidebar-top .sidebar {
order: -1;
}
.wrapper.single-column .sidebar {
grid-column: 1 / -1;
}
@@ -730,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 {
@@ -398,6 +398,7 @@ export class SecurityViewStrategy extends ReactiveElement {
],
}
: {}),
mobile_position: "tabs",
content_label: hass.localize(
"ui.panel.lovelace.strategy.security.devices"
),
+21
View File
@@ -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.",
@@ -10535,7 +10539,24 @@
},
"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."
},