Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
638bd6d740 | ||
|
|
3c2acdb1a2 | ||
|
|
328629441b | ||
|
|
6573c08f37 | ||
|
|
dcfde89448 | ||
|
|
3016bbdfa2 | ||
|
|
630d7ff82e | ||
|
|
6874781340 | ||
|
|
be214acfd1 |
No files matched your search
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="76" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="76" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,153 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiAutoFix, mdiContentPaste, mdiViewGridPlusOutline } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { consume } from "../../../../common/decorators/consume";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import "../../../../components/ha-adaptive-dialog";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/item/ha-list-item-button";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
import { internationalizationContext } from "../../../../data/context";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import { DialogMixin } from "../../../../dialogs/dialog-mixin";
|
||||
import { generateDefaultSection } from "../../views/default-section";
|
||||
import { addSection } from "../config-util";
|
||||
import type { CreateSectionDialogParams } from "./show-create-section-dialog";
|
||||
import { showSelectSectionStrategyDialog } from "./show-create-section-dialog";
|
||||
|
||||
@customElement("hui-dialog-create-section")
|
||||
export class HuiDialogCreateSection extends DialogMixin<CreateSectionDialogParams>(
|
||||
LitElement
|
||||
) {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "dashboardSectionClipboard",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
private _clipboard?: LovelaceSectionRawConfig;
|
||||
|
||||
protected render() {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const localize = this._i18n.localize;
|
||||
|
||||
return html`
|
||||
<ha-adaptive-dialog
|
||||
open
|
||||
header-title=${localize("ui.panel.lovelace.editor.create_section.header")}
|
||||
>
|
||||
<ha-list-base>
|
||||
<ha-list-item-button @click=${this._createManual}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiViewGridPlusOutline}
|
||||
></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${localize("ui.panel.lovelace.editor.create_section.manual")}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${localize(
|
||||
"ui.panel.lovelace.editor.create_section.manual_description"
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button @click=${this._pickStrategy}>
|
||||
<ha-svg-icon slot="start" .path=${mdiAutoFix}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${localize("ui.panel.lovelace.editor.create_section.automatic")}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${localize(
|
||||
"ui.panel.lovelace.editor.create_section.automatic_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
${
|
||||
this._clipboard
|
||||
? html`
|
||||
<ha-list-item-button @click=${this._paste}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiContentPaste}
|
||||
></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${localize("ui.panel.lovelace.editor.create_section.paste")}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${localize(
|
||||
"ui.panel.lovelace.editor.create_section.paste_description"
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-list-base>
|
||||
</ha-adaptive-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _createManual() {
|
||||
const { lovelace, containerPath } = this.params!;
|
||||
this.closeDialog();
|
||||
lovelace.saveConfig(
|
||||
addSection(
|
||||
lovelace.config,
|
||||
containerPath,
|
||||
generateDefaultSection(this._i18n.localize, true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _pickStrategy() {
|
||||
showSelectSectionStrategyDialog(this, this.params!);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _paste() {
|
||||
if (!this._clipboard) {
|
||||
return;
|
||||
}
|
||||
const { lovelace, containerPath } = this.params!;
|
||||
const section = deepClone(this._clipboard);
|
||||
this.closeDialog();
|
||||
try {
|
||||
await lovelace.saveConfig(
|
||||
addSection(lovelace.config, containerPath, section)
|
||||
);
|
||||
} catch (_err: unknown) {
|
||||
lovelace.showToast({
|
||||
message: this._i18n.localize(
|
||||
"ui.panel.lovelace.editor.section.paste_error"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-adaptive-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
ha-list-base {
|
||||
--ha-list-padding: 0 0 var(--ha-space-3);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-create-section": HuiDialogCreateSection;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,38 @@
|
||||
import { mdiClose, mdiDotsVertical, mdiFileMoveOutline } from "@mdi/js";
|
||||
import {
|
||||
mdiCellphone,
|
||||
mdiClose,
|
||||
mdiDotsVertical,
|
||||
mdiFileMoveOutline,
|
||||
mdiMonitor,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { withViewTransition } from "../../../../common/util/view-transition";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import "../../../../components/ha-tab-group";
|
||||
import "../../../../components/ha-tab-group-tab";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import { isStrategySection } from "../../../../data/lovelace/config/section";
|
||||
import {
|
||||
DEFAULT_SECTION_COLUMN_SPAN,
|
||||
isStrategySection,
|
||||
} from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceStrategyConfig } from "../../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import { saveConfig } from "../../../../data/lovelace/config/types";
|
||||
@@ -32,6 +44,7 @@ import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
haStyleScrollbar,
|
||||
} from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import { cleanLegacyStrategyConfig } from "../../strategies/legacy-strategy";
|
||||
@@ -41,10 +54,13 @@ import {
|
||||
deleteAtPath,
|
||||
getAtPath,
|
||||
getViewPath,
|
||||
insertAtPath,
|
||||
moveAtPath,
|
||||
setAtPath,
|
||||
} from "../lovelace-path";
|
||||
import { showSelectViewDialog } from "../select-view/show-select-view-dialog";
|
||||
import "../../sections/hui-section";
|
||||
import "../../sections/hui-section-background";
|
||||
import "./hui-section-settings-editor";
|
||||
import "./hui-section-visibility-editor";
|
||||
import "./hui-section-strategy-element-editor";
|
||||
@@ -53,10 +69,19 @@ import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import type { EditSectionDialogParams } from "./show-edit-section-dialog";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import { getViewType } from "../../views/get-view-type";
|
||||
import { SECTIONS_VIEW_LAYOUT } from "../../views/const";
|
||||
import { DEFAULT_MAX_COLUMNS, SECTIONS_VIEW_LAYOUT } from "../../views/const";
|
||||
|
||||
const TABS = ["tab-strategy", "tab-appearance", "tab-visibility"] as const;
|
||||
|
||||
type PreviewMode = "desktop" | "mobile";
|
||||
|
||||
const PREVIEW_MODES: { mode: PreviewMode; iconPath: string }[] = [
|
||||
{ mode: "mobile", iconPath: mdiCellphone },
|
||||
{ mode: "desktop", iconPath: mdiMonitor },
|
||||
];
|
||||
|
||||
const TWO_COLUMNS_MIN_WIDTH = 2 * 320 + 3 * 32;
|
||||
|
||||
@customElement("hui-dialog-edit-section")
|
||||
export class HuiDialogEditSection
|
||||
extends DirtyStateProviderMixin<LovelaceSectionRawConfig>()(LitElement)
|
||||
@@ -66,6 +91,8 @@ export class HuiDialogEditSection
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public large = false;
|
||||
|
||||
@state() private _params?: EditSectionDialogParams;
|
||||
|
||||
@state() private _config?: LovelaceSectionRawConfig;
|
||||
@@ -82,6 +109,15 @@ export class HuiDialogEditSection
|
||||
|
||||
@state() private _yamlError = false;
|
||||
|
||||
@storage({
|
||||
key: "dashboardSectionPreviewMode",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _storedPreviewMode?: PreviewMode;
|
||||
|
||||
@state() private _defaultPreviewMode: PreviewMode = "mobile";
|
||||
|
||||
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
|
||||
|
||||
@query("hui-section-strategy-element-editor")
|
||||
@@ -103,10 +139,9 @@ export class HuiDialogEditSection
|
||||
|
||||
this.lovelace = params.lovelace;
|
||||
|
||||
this._config = getAtPath<LovelaceSectionRawConfig>(
|
||||
params.lovelaceConfig,
|
||||
params.path
|
||||
);
|
||||
this._config =
|
||||
params.newSection ??
|
||||
getAtPath<LovelaceSectionRawConfig>(params.lovelaceConfig, params.path);
|
||||
this._currTab =
|
||||
this._config && isStrategySection(this._config)
|
||||
? "tab-strategy"
|
||||
@@ -116,7 +151,18 @@ export class HuiDialogEditSection
|
||||
params.lovelaceConfig,
|
||||
viewPath
|
||||
);
|
||||
this._initDirtyTracking({ type: "deep" }, this._config);
|
||||
if (params.newSection) {
|
||||
this._initDirtyTracking({ type: "deep" }, {});
|
||||
this._updateDirtyState(params.newSection);
|
||||
} else {
|
||||
this._initDirtyTracking({ type: "deep" }, this._config);
|
||||
}
|
||||
this._defaultPreviewMode =
|
||||
window.innerWidth >= TWO_COLUMNS_MIN_WIDTH ? "desktop" : "mobile";
|
||||
}
|
||||
|
||||
private get _previewMode(): PreviewMode {
|
||||
return this._storedPreviewMode ?? this._defaultPreviewMode;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -126,11 +172,13 @@ export class HuiDialogEditSection
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this.large = false;
|
||||
this._yamlMode = false;
|
||||
this._config = undefined;
|
||||
this._currTab = TABS[0];
|
||||
this._strategyError = false;
|
||||
this._yamlError = false;
|
||||
this._discardDirtyStateChanges();
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -139,19 +187,23 @@ export class HuiDialogEditSection
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const heading = this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_section.header"
|
||||
);
|
||||
|
||||
const strategy = isStrategySection(this._config)
|
||||
? cleanLegacyStrategyConfig(this._config.strategy)
|
||||
: undefined;
|
||||
const heading = this._computeHeading(strategy);
|
||||
const tabs = strategy ? TABS : TABS.filter((tab) => tab !== "tab-strategy");
|
||||
const currentTab =
|
||||
!strategy && this._currTab === "tab-strategy"
|
||||
? "tab-appearance"
|
||||
: this._currTab;
|
||||
|
||||
const columnSpan = Math.min(
|
||||
this._config.column_span ?? DEFAULT_SECTION_COLUMN_SPAN,
|
||||
this._viewConfig?.max_columns || DEFAULT_MAX_COLUMNS
|
||||
);
|
||||
const singleColumn = columnSpan === 1;
|
||||
const previewColumnSpan = this._previewMode === "desktop" ? columnSpan : 1;
|
||||
|
||||
let content: TemplateResult<1> | typeof nothing = nothing;
|
||||
|
||||
if (this._yamlMode) {
|
||||
@@ -201,65 +253,160 @@ export class HuiDialogEditSection
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.width=${this.large ? "full" : "large"}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@keydown=${this._ignoreKeydown}
|
||||
@closed=${this._dialogClosed}
|
||||
class=${classMap({
|
||||
"yaml-mode": this._yamlMode,
|
||||
})}
|
||||
>
|
||||
<ha-dialog-header show-border slot="header">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this._cancel}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title">${heading}</span>
|
||||
<ha-dropdown
|
||||
slot="actionItems"
|
||||
placement="bottom-end"
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="move-to-view">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFileMoveOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_view.move_to_view"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
${
|
||||
!this._yamlMode
|
||||
? html`
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${tabs.map(
|
||||
(tab) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${tab}
|
||||
.active=${currentTab === tab}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.${tab.replace("-", "_")}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
<ha-icon-button
|
||||
slot="headerNavigationIcon"
|
||||
@click=${this._cancel}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span
|
||||
slot="headerTitle"
|
||||
class="title-enlargeable"
|
||||
@click=${this._enlarge}
|
||||
>${heading}</span
|
||||
>
|
||||
${
|
||||
this._params.newSection
|
||||
? nothing
|
||||
: html`
|
||||
<ha-dropdown
|
||||
slot="headerActionItems"
|
||||
placement="bottom-end"
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="move-to-view">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFileMoveOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_view.move_to_view"
|
||||
)}
|
||||
</ha-tab-group>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-dialog-header>
|
||||
${this._yamlMode ? content : cache(content)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`
|
||||
}
|
||||
<div class="content">
|
||||
<div class="element-editor ha-scrollbar">
|
||||
${
|
||||
!this._yamlMode
|
||||
? html`
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${tabs.map(
|
||||
(tab) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${tab}
|
||||
.active=${currentTab === tab}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.${tab.replace("-", "_")}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
</ha-tab-group>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${this._yamlMode ? content : cache(content)}
|
||||
</div>
|
||||
<div class="element-preview ha-scrollbar">
|
||||
<div class="preview-header">
|
||||
<span id="preview-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_section.preview"
|
||||
)}
|
||||
</span>
|
||||
${
|
||||
singleColumn
|
||||
? nothing
|
||||
: html`
|
||||
<div
|
||||
class="preview-modes"
|
||||
role="group"
|
||||
aria-labelledby="preview-label"
|
||||
>
|
||||
${PREVIEW_MODES.map(
|
||||
({ mode, iconPath }) => html`
|
||||
<button
|
||||
id="preview-mode-${mode}"
|
||||
class=${classMap({
|
||||
"preview-mode": true,
|
||||
active: this._previewMode === mode,
|
||||
})}
|
||||
aria-pressed=${this._previewMode === mode}
|
||||
aria-label=${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.preview_${mode}`
|
||||
)}
|
||||
.mode=${mode}
|
||||
@click=${this._previewModeClicked}
|
||||
>
|
||||
<ha-svg-icon .path=${iconPath}></ha-svg-icon>
|
||||
</button>
|
||||
<ha-tooltip
|
||||
for="preview-mode-${mode}"
|
||||
placement="bottom"
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.preview_${mode}`
|
||||
)}
|
||||
</ha-tooltip>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
<div class="preview-frame">
|
||||
<div
|
||||
class=${classMap({
|
||||
"section-preview": true,
|
||||
wide: previewColumnSpan > 1,
|
||||
})}
|
||||
style=${styleMap({
|
||||
"--column-span": previewColumnSpan,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
class=${classMap({
|
||||
"section-container": true,
|
||||
"has-background": this._config.background !== undefined,
|
||||
blur: this._yamlError || this._strategyError,
|
||||
})}
|
||||
>
|
||||
${
|
||||
this._config.background !== undefined
|
||||
? html`
|
||||
<hui-section-background
|
||||
.hass=${this.hass}
|
||||
.background=${this._config.background}
|
||||
.theme=${this._config.theme}
|
||||
></hui-section-background>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<hui-section
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
preview
|
||||
></hui-section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@@ -318,6 +465,37 @@ export class HuiDialogEditSection
|
||||
this._updateDirtyState(this._config);
|
||||
}
|
||||
|
||||
private _computeHeading(strategy?: LovelaceStrategyConfig) {
|
||||
const header = this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_section.header"
|
||||
);
|
||||
if (!strategy) {
|
||||
return header;
|
||||
}
|
||||
const strategyName = this.hass.localize(
|
||||
`ui.panel.lovelace.editor.section_strategy.${strategy.type}.name`
|
||||
);
|
||||
if (!strategyName) {
|
||||
return header;
|
||||
}
|
||||
return this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_section.typed_header",
|
||||
{ type: strategyName }
|
||||
);
|
||||
}
|
||||
|
||||
private _previewModeClicked(ev: Event) {
|
||||
this._storedPreviewMode = (
|
||||
ev.currentTarget as HTMLElement & { mode: PreviewMode }
|
||||
).mode;
|
||||
}
|
||||
|
||||
private _enlarge() {
|
||||
withViewTransition(() => {
|
||||
this.large = !this.large;
|
||||
});
|
||||
}
|
||||
|
||||
private _handleTabChanged(ev: CustomEvent): void {
|
||||
const newTab = ev.detail.name;
|
||||
if (newTab === this._currTab) {
|
||||
@@ -511,11 +689,13 @@ export class HuiDialogEditSection
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const newConfig = setAtPath(
|
||||
this._params.lovelaceConfig,
|
||||
this._params.path,
|
||||
this._config
|
||||
);
|
||||
const newConfig = this._params.newSection
|
||||
? insertAtPath(
|
||||
this._params.lovelaceConfig,
|
||||
this._params.path,
|
||||
this._config
|
||||
)
|
||||
: setAtPath(this._params.lovelaceConfig, this._params.path, this._config);
|
||||
|
||||
this._params.saveConfig(newConfig);
|
||||
this._markDirtyStateClean();
|
||||
@@ -526,15 +706,153 @@ export class HuiDialogEditSection
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
--code-mirror-max-height: calc(100vh - 209px);
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--dialog-content-padding: var(--ha-space-6);
|
||||
--dialog-content-padding: var(--ha-space-2);
|
||||
}
|
||||
ha-dialog.yaml-mode {
|
||||
--dialog-content-padding: 0;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.gui-mode-button {
|
||||
margin-inline-end: auto;
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.content {
|
||||
gap: var(--ha-space-3);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||
:host([large]) .content {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content .preview-frame {
|
||||
padding: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.content .section-preview {
|
||||
--preview-column-width: 390px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: var(--preview-column-width);
|
||||
}
|
||||
.content .section-preview.wide {
|
||||
max-width: none;
|
||||
width: calc(
|
||||
var(--column-span) * var(--preview-column-width) +
|
||||
(var(--column-span) - 1) *
|
||||
var(--ha-view-sections-column-gap, 32px)
|
||||
);
|
||||
}
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ha-space-2);
|
||||
min-height: 32px;
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
.preview-modes {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
background: var(--ha-color-fill-neutral-normal-resting);
|
||||
}
|
||||
.preview-mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
background: none;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
.preview-mode:hover {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.preview-mode.active {
|
||||
color: var(--primary-text-color);
|
||||
background: var(--card-background-color);
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 0.2);
|
||||
}
|
||||
.preview-mode:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.content .element-editor {
|
||||
padding-inline-start: var(--ha-space-1);
|
||||
padding-inline-end: var(--ha-space-2);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
.content {
|
||||
flex-direction: row;
|
||||
max-height: var(--code-mirror-max-height);
|
||||
}
|
||||
.content > .element-editor {
|
||||
padding-inline-end: var(--ha-space-4);
|
||||
max-width: 500px;
|
||||
}
|
||||
.content > .element-editor,
|
||||
.content > .element-preview {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
height: auto;
|
||||
}
|
||||
.content .preview-frame {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
}
|
||||
.element-editor {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.blur {
|
||||
filter: blur(2px) grayscale(100%);
|
||||
}
|
||||
.element-preview {
|
||||
position: relative;
|
||||
height: max-content;
|
||||
background: var(--primary-background-color);
|
||||
padding: 4px;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
.section-container {
|
||||
position: relative;
|
||||
}
|
||||
.section-container.has-background {
|
||||
padding: var(--ha-space-2);
|
||||
border-radius: var(
|
||||
--ha-section-border-radius,
|
||||
var(--ha-border-radius-xl)
|
||||
);
|
||||
}
|
||||
.section-container hui-section {
|
||||
display: block;
|
||||
}
|
||||
ha-tab-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-tab-group-tab {
|
||||
flex: 1;
|
||||
@@ -543,6 +861,14 @@ export class HuiDialogEditSection
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.gui-mode-button {
|
||||
margin-right: auto;
|
||||
margin-inline-end: auto;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.title-enlargeable {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-adaptive-dialog";
|
||||
import "../../../../components/ha-icon-button-arrow-prev";
|
||||
import "../../../../components/ha-ripple";
|
||||
import type { LovelaceStrategySectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import { CUSTOM_TYPE_PREFIX } from "../../../../data/lovelace_custom_cards";
|
||||
import { getCustomStrategiesForType } from "../../../../data/lovelace_custom_strategies";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../sections/hui-section";
|
||||
import { getLovelaceStrategy } from "../../strategies/get-strategy";
|
||||
import type { LovelaceSectionStrategy } from "../../strategies/types";
|
||||
import { getAtPath } from "../lovelace-path";
|
||||
import type { CreateSectionDialogParams } from "./show-create-section-dialog";
|
||||
import { showCreateSectionDialog } from "./show-create-section-dialog";
|
||||
import { showEditSectionDialog } from "./show-edit-section-dialog";
|
||||
|
||||
interface BuiltInSectionStrategy {
|
||||
type: "common-controls";
|
||||
isAvailable?: (hass: HomeAssistant) => boolean;
|
||||
}
|
||||
|
||||
const BUILT_IN_SECTION_STRATEGIES: BuiltInSectionStrategy[] = [
|
||||
{
|
||||
type: "common-controls",
|
||||
isAvailable: (hass) => isComponentLoaded(hass.config, "usage_prediction"),
|
||||
},
|
||||
];
|
||||
|
||||
interface StrategyOption {
|
||||
type: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
@customElement("hui-dialog-select-section-strategy")
|
||||
export class HuiDialogSelectSectionStrategy
|
||||
extends LitElement
|
||||
implements HassDialog<CreateSectionDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: CreateSectionDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _previews: Record<string, LovelaceStrategySectionConfig> =
|
||||
{};
|
||||
|
||||
public showDialog(params: CreateSectionDialogParams): void {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
this._previews = {};
|
||||
this._loadPreviews(params);
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this._previews = {};
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _availableBuiltInTypes() {
|
||||
return BUILT_IN_SECTION_STRATEGIES.filter(
|
||||
(strategy) => strategy.isAvailable?.(this.hass) ?? true
|
||||
).map((strategy) => strategy.type);
|
||||
}
|
||||
|
||||
private _builtInOptions(): StrategyOption[] {
|
||||
return this._availableBuiltInTypes().map((type) => ({
|
||||
type,
|
||||
name: this.hass.localize(
|
||||
`ui.panel.lovelace.editor.section_strategy.${type}.name`
|
||||
),
|
||||
description: this.hass.localize(
|
||||
`ui.panel.lovelace.editor.section_strategy.${type}.description`
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
private _customOptions(): StrategyOption[] {
|
||||
return getCustomStrategiesForType("section").map((strategy) => ({
|
||||
type: CUSTOM_TYPE_PREFIX + strategy.type,
|
||||
name: strategy.name || strategy.type,
|
||||
description: strategy.description || "",
|
||||
}));
|
||||
}
|
||||
|
||||
private async _loadPreviews(params: CreateSectionDialogParams) {
|
||||
const types = [
|
||||
...this._availableBuiltInTypes(),
|
||||
...getCustomStrategiesForType("section").map(
|
||||
(strategy) => CUSTOM_TYPE_PREFIX + strategy.type
|
||||
),
|
||||
];
|
||||
await Promise.all(
|
||||
types.map(async (type) => {
|
||||
try {
|
||||
const strategy = (await getLovelaceStrategy(
|
||||
"section",
|
||||
type
|
||||
)) as LovelaceSectionStrategy;
|
||||
const stubConfig = await strategy.getStubConfig?.(this.hass);
|
||||
if (!stubConfig || this._params !== params) {
|
||||
return;
|
||||
}
|
||||
this._previews = {
|
||||
...this._previews,
|
||||
[type]: { strategy: stubConfig },
|
||||
};
|
||||
} catch (_err: unknown) {
|
||||
// The tile is shown without a preview
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const customOptions = this._customOptions();
|
||||
|
||||
return html`
|
||||
<ha-adaptive-dialog
|
||||
.open=${this._open}
|
||||
width="large"
|
||||
@closed=${this._dialogClosed}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.select_section_strategy.header"
|
||||
)}
|
||||
>
|
||||
<ha-icon-button-arrow-prev
|
||||
slot="headerNavigationIcon"
|
||||
@click=${this._back}
|
||||
></ha-icon-button-arrow-prev>
|
||||
${
|
||||
customOptions.length
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.select_section_strategy.built_in"
|
||||
)}
|
||||
</h3>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<div class="options">
|
||||
${this._builtInOptions().map((option) => this._renderOption(option))}
|
||||
</div>
|
||||
${
|
||||
customOptions.length
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.select_section_strategy.custom"
|
||||
)}
|
||||
</h3>
|
||||
<div class="options">
|
||||
${customOptions.map((option) => this._renderOption(option))}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-adaptive-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderOption(option: StrategyOption) {
|
||||
const preview = this._previews[option.type];
|
||||
return html`
|
||||
<div
|
||||
class="option"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label=${option.name}
|
||||
data-type=${option.type}
|
||||
@click=${this._strategyPicked}
|
||||
@keydown=${this._handleKeyDown}
|
||||
>
|
||||
<div class="header">
|
||||
<h4>${option.name}</h4>
|
||||
<p>${option.description}</p>
|
||||
</div>
|
||||
${
|
||||
preview
|
||||
? html`
|
||||
<div class="preview" inert>
|
||||
<hui-section
|
||||
.hass=${this.hass}
|
||||
.config=${preview}
|
||||
preview
|
||||
></hui-section>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-ripple></ha-ripple>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
(ev.currentTarget as HTMLElement).click();
|
||||
}
|
||||
}
|
||||
|
||||
private _back() {
|
||||
showCreateSectionDialog(this, this._params!);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _strategyPicked(ev: Event) {
|
||||
const type = (ev.currentTarget as HTMLElement).dataset.type!;
|
||||
const { lovelace, containerPath } = this._params!;
|
||||
const sectionsPath = [...containerPath, "sections"];
|
||||
const sectionCount =
|
||||
getAtPath<unknown[]>(lovelace.config, sectionsPath)?.length ?? 0;
|
||||
|
||||
showEditSectionDialog(this, {
|
||||
lovelace,
|
||||
lovelaceConfig: lovelace.config,
|
||||
saveConfig: (newConfig) => {
|
||||
lovelace.saveConfig(newConfig);
|
||||
},
|
||||
path: [...sectionsPath, sectionCount],
|
||||
newSection: this._previews[type] ?? { strategy: { type } },
|
||||
});
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
h3 {
|
||||
margin: 0 0 var(--ha-space-3) 0;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
.options + h3 {
|
||||
margin-top: var(--ha-space-6);
|
||||
}
|
||||
.options {
|
||||
display: grid;
|
||||
gap: var(--ha-space-4);
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
|
||||
}
|
||||
.option {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.option:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.header {
|
||||
flex: 1;
|
||||
padding: var(--ha-space-3) var(--ha-space-4);
|
||||
}
|
||||
h4 {
|
||||
margin: 0 0 var(--ha-space-1) 0;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.preview {
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
padding: var(--ha-space-3);
|
||||
background: var(--primary-background-color);
|
||||
pointer-events: none;
|
||||
mask-image: linear-gradient(to bottom, black 75%, transparent);
|
||||
}
|
||||
hui-section {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-select-section-strategy": HuiDialogSelectSectionStrategy;
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,71 @@ import {
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import { DEFAULT_MAX_COLUMNS } from "../../views/const";
|
||||
|
||||
const COLUMN_SPAN_NAMES = ["single", "double", "triple"] as const;
|
||||
|
||||
const MAX_NAMED_COLUMN_SPAN = COLUMN_SPAN_NAMES.length + 1;
|
||||
|
||||
const computeColumnSpanLabel = (
|
||||
span: number,
|
||||
maxColumns: number,
|
||||
localize: LocalizeFunc
|
||||
) => {
|
||||
if (span >= maxColumns) {
|
||||
return localize(
|
||||
"ui.panel.lovelace.editor.edit_section.settings.column_span_options.full"
|
||||
);
|
||||
}
|
||||
return localize(
|
||||
`ui.panel.lovelace.editor.edit_section.settings.column_span_options.${COLUMN_SPAN_NAMES[span - 1]}`
|
||||
);
|
||||
};
|
||||
|
||||
const computeColumnSpanSchemas = (
|
||||
maxColumns: number,
|
||||
localize: LocalizeFunc
|
||||
) => {
|
||||
if (maxColumns <= 1) {
|
||||
return [];
|
||||
}
|
||||
if (maxColumns > MAX_NAMED_COLUMN_SPAN) {
|
||||
return [
|
||||
{
|
||||
name: "column_span",
|
||||
selector: {
|
||||
number: { min: 1, max: maxColumns, slider_ticks: true },
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
const options = Array.from({ length: maxColumns }, (_, i) => {
|
||||
const span = i + 1;
|
||||
return {
|
||||
value: String(span),
|
||||
label: computeColumnSpanLabel(span, maxColumns, localize),
|
||||
image: {
|
||||
src: `/static/images/form/section_column_span_${span}_of_${maxColumns}.svg`,
|
||||
src_dark: `/static/images/form/section_column_span_${span}_of_${maxColumns}_dark.svg`,
|
||||
flip_rtl: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
return [
|
||||
{
|
||||
name: "column_span",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
box_max_columns:
|
||||
maxColumns === MAX_NAMED_COLUMN_SPAN ? 2 : maxColumns,
|
||||
options,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
|
||||
interface SettingsData {
|
||||
column_span?: number;
|
||||
column_span?: string | number;
|
||||
background_enabled?: boolean;
|
||||
background_color?: string;
|
||||
background_opacity?: number;
|
||||
@@ -36,20 +99,30 @@ export class HuiDialogEditSection extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public viewConfig!: LovelaceViewConfig;
|
||||
|
||||
private get _maxColumns() {
|
||||
return this.viewConfig.max_columns || DEFAULT_MAX_COLUMNS;
|
||||
}
|
||||
|
||||
private get _displayedColumnSpan() {
|
||||
return String(
|
||||
Math.min(
|
||||
this.config.column_span ?? DEFAULT_SECTION_COLUMN_SPAN,
|
||||
this._maxColumns
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private get _columnSpanFormValue() {
|
||||
if (this._maxColumns > MAX_NAMED_COLUMN_SPAN) {
|
||||
return Number(this._displayedColumnSpan);
|
||||
}
|
||||
return this._displayedColumnSpan;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(maxColumns: number, localize: LocalizeFunc) =>
|
||||
(localize: LocalizeFunc, maxColumns: number) =>
|
||||
[
|
||||
{
|
||||
name: "column_span",
|
||||
default: DEFAULT_SECTION_COLUMN_SPAN,
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: maxColumns,
|
||||
slider_ticks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
...computeColumnSpanSchemas(maxColumns, localize),
|
||||
{
|
||||
name: "background_enabled",
|
||||
selector: { boolean: {} },
|
||||
@@ -109,17 +182,14 @@ export class HuiDialogEditSection extends LitElement {
|
||||
const background = resolveSectionBackground(this.config.background);
|
||||
|
||||
const data: SettingsData = {
|
||||
column_span: this.config.column_span,
|
||||
column_span: this._columnSpanFormValue,
|
||||
background_enabled: backgroundEnabled,
|
||||
background_color: background?.color,
|
||||
background_opacity: background?.opacity,
|
||||
theme: this.config.theme,
|
||||
};
|
||||
|
||||
const schema = this._schema(
|
||||
this.viewConfig.max_columns || DEFAULT_MAX_COLUMNS,
|
||||
this._localize
|
||||
);
|
||||
const schema = this._schema(this._localize, this._maxColumns);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@@ -154,10 +224,15 @@ export class HuiDialogEditSection extends LitElement {
|
||||
...this.config,
|
||||
};
|
||||
|
||||
if (newData.column_span) {
|
||||
newConfig.column_span = newData.column_span;
|
||||
} else {
|
||||
if (newData.column_span === undefined) {
|
||||
delete newConfig.column_span;
|
||||
} else if (String(newData.column_span) !== this._displayedColumnSpan) {
|
||||
const columnSpan = Number(newData.column_span);
|
||||
if (columnSpan === DEFAULT_SECTION_COLUMN_SPAN) {
|
||||
delete newConfig.column_span;
|
||||
} else {
|
||||
newConfig.column_span = columnSpan;
|
||||
}
|
||||
}
|
||||
|
||||
if (newData.background_enabled) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { Lovelace } from "../../types";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
|
||||
export interface CreateSectionDialogParams {
|
||||
lovelace: Lovelace;
|
||||
containerPath: LovelacePath;
|
||||
}
|
||||
|
||||
export const showCreateSectionDialog = (
|
||||
element: HTMLElement,
|
||||
params: CreateSectionDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-create-section",
|
||||
dialogImport: () => import("./hui-dialog-create-section"),
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
|
||||
export const showSelectSectionStrategyDialog = (
|
||||
element: HTMLElement,
|
||||
params: CreateSectionDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-select-section-strategy",
|
||||
dialogImport: () => import("./hui-dialog-select-section-strategy"),
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { Lovelace } from "../../types";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
@@ -8,6 +9,7 @@ export interface EditSectionDialogParams {
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
path: LovelacePath;
|
||||
newSection?: LovelaceSectionRawConfig;
|
||||
}
|
||||
|
||||
const importEditSectionDialog = () => import("./hui-dialog-edit-section");
|
||||
|
||||
@@ -3,6 +3,8 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../../../common/dom/media_query";
|
||||
import { clamp } from "../../../../common/number/clamp";
|
||||
import { slugify } from "../../../../common/string/slugify";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
@@ -32,6 +34,37 @@ declare global {
|
||||
const VALID_PATH_REGEX = /^[a-zA-Z0-9_-]+$/;
|
||||
const INTEGER_REGEX = /^[0-9]+$/;
|
||||
|
||||
const MAX_COLUMNS_PRESETS = [1, 2, 3, 4] as const;
|
||||
const MAX_COLUMNS_MORE = "more";
|
||||
const MIN_CUSTOM_MAX_COLUMNS = MAX_COLUMNS_PRESETS.length + 1;
|
||||
const MAX_CUSTOM_MAX_COLUMNS = 10;
|
||||
|
||||
interface MaxColumnsFormData {
|
||||
max_columns: string;
|
||||
max_columns_custom?: number;
|
||||
}
|
||||
|
||||
const maxColumnsToFormData = (maxColumns: number): MaxColumnsFormData => {
|
||||
if (maxColumns <= MAX_COLUMNS_PRESETS.length) {
|
||||
return {
|
||||
max_columns: String(maxColumns),
|
||||
max_columns_custom: MIN_CUSTOM_MAX_COLUMNS,
|
||||
};
|
||||
}
|
||||
return { max_columns: MAX_COLUMNS_MORE, max_columns_custom: maxColumns };
|
||||
};
|
||||
|
||||
const maxColumnsFromFormData = (data: MaxColumnsFormData): number => {
|
||||
if (data.max_columns !== MAX_COLUMNS_MORE) {
|
||||
return Number(data.max_columns);
|
||||
}
|
||||
return clamp(
|
||||
data.max_columns_custom ?? MIN_CUSTOM_MAX_COLUMNS,
|
||||
MIN_CUSTOM_MAX_COLUMNS,
|
||||
MAX_CUSTOM_MAX_COLUMNS
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("hui-view-editor")
|
||||
export class HuiViewEditor extends LitElement {
|
||||
@consumeLocalize()
|
||||
@@ -43,10 +76,27 @@ export class HuiViewEditor extends LitElement {
|
||||
|
||||
@state() private _error: Record<string, string> | undefined;
|
||||
|
||||
@state() private _narrow = false;
|
||||
|
||||
private _unsubMql?: () => void;
|
||||
|
||||
private _suggestedPath = false;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._unsubMql = listenMediaQuery("(max-width: 600px)", (matches) => {
|
||||
this._narrow = matches;
|
||||
});
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubMql?.();
|
||||
this._unsubMql = undefined;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
(localize: LocalizeFunc, narrow: boolean) =>
|
||||
[
|
||||
{
|
||||
name: "type",
|
||||
@@ -101,13 +151,33 @@ export class HuiViewEditor extends LitElement {
|
||||
schema: [
|
||||
{
|
||||
name: "max_columns",
|
||||
default: DEFAULT_MAX_COLUMNS,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
box_max_columns: narrow ? 3 : 5,
|
||||
options: [
|
||||
...MAX_COLUMNS_PRESETS.map((count) => ({
|
||||
value: String(count),
|
||||
label: String(count),
|
||||
})),
|
||||
{
|
||||
value: MAX_COLUMNS_MORE,
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.edit_view.max_columns_more"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "max_columns_custom",
|
||||
visible: { field: "max_columns", value: MAX_COLUMNS_MORE },
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: 10,
|
||||
mode: "slider",
|
||||
slider_ticks: true,
|
||||
min: MIN_CUSTOM_MAX_COLUMNS,
|
||||
max: MAX_CUSTOM_MAX_COLUMNS,
|
||||
mode: "box",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -132,6 +202,10 @@ export class HuiViewEditor extends LitElement {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private get _maxColumns(): number {
|
||||
return this._config.max_columns ?? DEFAULT_MAX_COLUMNS;
|
||||
}
|
||||
|
||||
get _type(): string {
|
||||
return getViewType(this._config);
|
||||
}
|
||||
@@ -139,6 +213,7 @@ export class HuiViewEditor extends LitElement {
|
||||
protected render() {
|
||||
const data = {
|
||||
...this._config,
|
||||
...maxColumnsToFormData(this._maxColumns),
|
||||
type: this._type,
|
||||
theme:
|
||||
this._config.theme?.toLowerCase() === "backend-selected"
|
||||
@@ -149,7 +224,7 @@ export class HuiViewEditor extends LitElement {
|
||||
return html`
|
||||
<ha-form
|
||||
.data=${data}
|
||||
.schema=${this._schema(this._localize)}
|
||||
.schema=${this._schema(this._localize, this._narrow)}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
.computeError=${this._computeError}
|
||||
@@ -160,7 +235,23 @@ export class HuiViewEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
const config = ev.detail.value as LovelaceViewConfig;
|
||||
const {
|
||||
max_columns: maxColumnsChoice,
|
||||
max_columns_custom: maxColumnsCustom,
|
||||
...rest
|
||||
} = ev.detail.value as Omit<LovelaceViewConfig, "max_columns"> &
|
||||
MaxColumnsFormData;
|
||||
const config: LovelaceViewConfig = rest;
|
||||
|
||||
const maxColumns = maxColumnsFromFormData({
|
||||
max_columns: maxColumnsChoice,
|
||||
max_columns_custom: maxColumnsCustom,
|
||||
});
|
||||
if (maxColumns !== this._maxColumns) {
|
||||
config.max_columns = maxColumns;
|
||||
} else if (this._config.max_columns !== undefined) {
|
||||
config.max_columns = this._config.max_columns;
|
||||
}
|
||||
|
||||
if (config.type !== SECTIONS_VIEW_LAYOUT) {
|
||||
delete config.max_columns;
|
||||
@@ -211,6 +302,7 @@ export class HuiViewEditor extends LitElement {
|
||||
case "show_icon_and_title":
|
||||
case "subview":
|
||||
case "max_columns":
|
||||
case "max_columns_custom":
|
||||
case "dense_section_placement":
|
||||
case "top_margin":
|
||||
case "section_specifics":
|
||||
@@ -231,6 +323,7 @@ export class HuiViewEditor extends LitElement {
|
||||
case "path":
|
||||
case "show_icon_and_title":
|
||||
case "subview":
|
||||
case "max_columns":
|
||||
case "dense_section_placement":
|
||||
case "top_margin":
|
||||
return this._localize(
|
||||
|
||||
@@ -279,7 +279,8 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
}
|
||||
|
||||
private async _initializeConfig() {
|
||||
let sectionConfig = { ...this.config };
|
||||
const config = this.config;
|
||||
let sectionConfig = { ...config };
|
||||
let isStrategy = false;
|
||||
|
||||
if (isStrategySection(sectionConfig)) {
|
||||
@@ -288,6 +289,9 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
sectionConfig,
|
||||
this.hass!
|
||||
);
|
||||
if (config !== this.config) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sectionConfig = {
|
||||
|
||||
@@ -35,7 +35,11 @@ export type LovelaceDashboardStrategyGetCreateSuggestions = (
|
||||
|
||||
export interface LovelaceViewStrategy extends LovelaceStrategy<LovelaceViewConfig> {}
|
||||
|
||||
export interface LovelaceSectionStrategy extends LovelaceStrategy<LovelaceSectionConfig> {}
|
||||
export interface LovelaceSectionStrategy extends LovelaceStrategy<LovelaceSectionConfig> {
|
||||
getStubConfig?(
|
||||
hass: HomeAssistant
|
||||
): LovelaceStrategyConfig | Promise<LovelaceStrategyConfig>;
|
||||
}
|
||||
|
||||
export interface LovelaceStrategyEditor extends LovelaceGenericElementEditor {
|
||||
setConfig(config: LovelaceStrategyConfig): void;
|
||||
|
||||
@@ -40,6 +40,10 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
|
||||
);
|
||||
}
|
||||
|
||||
public static getStubConfig(): CommonControlsSectionStrategyConfig {
|
||||
return { type: "common-controls" };
|
||||
}
|
||||
|
||||
static async generate(
|
||||
config: CommonControlsSectionStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -30,6 +30,7 @@ import "../components/hui-section-edit-mode";
|
||||
import { addSection } from "../editor/config-util";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { getAtPath, moveAtPath } from "../editor/lovelace-path";
|
||||
import { showCreateSectionDialog } from "../editor/section-editor/show-create-section-dialog";
|
||||
import type { HuiSection } from "../sections/hui-section";
|
||||
import "../sections/hui-section-background";
|
||||
import type { Lovelace } from "../types";
|
||||
@@ -474,12 +475,10 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
}
|
||||
|
||||
private _createSection(): void {
|
||||
const newConfig = addSection(
|
||||
this.lovelace!.config,
|
||||
this.path!,
|
||||
generateDefaultSection(this.hass.localize, true)
|
||||
);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
showCreateSectionDialog(this, {
|
||||
lovelace: this.lovelace!,
|
||||
containerPath: this.path!,
|
||||
});
|
||||
}
|
||||
|
||||
private _sectionMoved(ev: CustomEvent) {
|
||||
|
||||
@@ -9972,7 +9972,10 @@
|
||||
},
|
||||
"show_icon_and_title": "Show icon and title",
|
||||
"subview": "Subview",
|
||||
"max_columns": "Max number of sections wide",
|
||||
"max_columns": "Max sections side by side",
|
||||
"max_columns_helper": "Smaller screens fit fewer side by side, and phones stack them one under the other.",
|
||||
"max_columns_more": "More",
|
||||
"max_columns_custom": "Number of sections",
|
||||
"section_specifics": "Sections view specific settings",
|
||||
"dense_section_placement": "Dense section placement",
|
||||
"dense_section_placement_helper": "Will try to fill gaps with sections that fit the gap. This may make section placement less predictable.",
|
||||
@@ -10164,8 +10167,32 @@
|
||||
"error_section": {
|
||||
"title": "Error"
|
||||
},
|
||||
"section_strategy": {
|
||||
"common-controls": {
|
||||
"name": "Common controls",
|
||||
"description": "Shows the entities you use most, based on your activity."
|
||||
}
|
||||
},
|
||||
"create_section": {
|
||||
"header": "Create section",
|
||||
"manual": "Manual section",
|
||||
"manual_description": "Start empty and add your own cards.",
|
||||
"automatic": "[%key:ui::panel::lovelace::editor::section::automatic%]",
|
||||
"automatic_description": "Fills itself with cards and stays up to date.",
|
||||
"paste": "Paste section",
|
||||
"paste_description": "Add the section you copied."
|
||||
},
|
||||
"select_section_strategy": {
|
||||
"header": "Choose an automatic section",
|
||||
"built_in": "Built-in sections",
|
||||
"custom": "Community sections"
|
||||
},
|
||||
"edit_section": {
|
||||
"header": "Edit section",
|
||||
"header": "Section configuration",
|
||||
"typed_header": "{type} section configuration",
|
||||
"preview": "Preview",
|
||||
"preview_desktop": "Desktop preview",
|
||||
"preview_mobile": "Mobile preview",
|
||||
"tab_strategy": "Configuration",
|
||||
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_view::tab_visibility%]",
|
||||
"tab_appearance": "Appearance",
|
||||
@@ -10173,7 +10200,13 @@
|
||||
"edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]",
|
||||
"settings": {
|
||||
"column_span": "Width",
|
||||
"column_span_helper": "Larger sections will be made smaller to fit the display. (for example, on mobile devices)",
|
||||
"column_span_helper": "Smaller screens may show it narrower, and phones stack sections one under the other.",
|
||||
"column_span_options": {
|
||||
"single": "Single",
|
||||
"double": "Double",
|
||||
"triple": "Triple",
|
||||
"full": "Full width"
|
||||
},
|
||||
"background": "Background options",
|
||||
"background_enabled": "Background",
|
||||
"background_enabled_helper": "Display a colored background behind the section",
|
||||
|
||||