Compare commits

...

4 Commits

Author SHA1 Message Date
Aidan Timson
9f1e4ea942 Type fix 2026-04-13 12:37:16 +01:00
Aidan Timson
d46c686118 Integrated into toolbar layout 2026-04-13 12:19:30 +01:00
Aidan Timson
3aea6a13ec Reduce top position 2026-04-13 12:19:30 +01:00
Aidan Timson
f1c26dc747 Add floating support for badges 2026-04-13 12:19:30 +01:00
7 changed files with 193 additions and 28 deletions

View File

@@ -93,6 +93,7 @@ export class HaSelectSelector extends LitElement {
<ha-select-box
.options=${options}
.value=${this.value as string | undefined}
.disabled=${this.disabled}
@value-changed=${this._selectChanged}
.maxColumns=${this.selector.select?.box_max_columns}
.hass=${this.hass}

View File

@@ -32,7 +32,7 @@ export interface LovelaceViewBackgroundConfig {
export interface LovelaceViewHeaderConfig {
card?: LovelaceCardConfig;
layout?: "start" | "center" | "responsive";
layout?: "start" | "center" | "responsive" | "integrated";
badges_position?: "bottom" | "top";
badges_wrap?: "wrap" | "scroll";
}

View File

@@ -1,3 +1,4 @@
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -9,15 +10,13 @@ import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type {
LovelaceViewConfig,
LovelaceViewHeaderConfig,
} from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceViewHeaderConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import {
DEFAULT_VIEW_HEADER_BADGES_POSITION,
DEFAULT_VIEW_HEADER_BADGES_WRAP,
DEFAULT_VIEW_HEADER_LAYOUT,
VIEW_HEADER_LAYOUT_INTEGRATED,
} from "../../views/hui-view-header";
import { listenMediaQuery } from "../../../../common/dom/media_query";
@@ -29,6 +28,8 @@ export class HuiViewHeaderSettingsEditor extends LitElement {
@state({ attribute: false }) private narrow = false;
@state() private _selectedLayout = DEFAULT_VIEW_HEADER_LAYOUT;
private _unsubMql?: () => void;
connectedCallback(): void {
@@ -44,16 +45,32 @@ export class HuiViewHeaderSettingsEditor extends LitElement {
this._unsubMql = undefined;
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
if (changedProperties.has("config")) {
this._selectedLayout = this.config?.layout ?? DEFAULT_VIEW_HEADER_LAYOUT;
}
}
private _schema = memoizeOne(
(localize: LocalizeFunc, isRTL: boolean, narrow: boolean) =>
(
localize: LocalizeFunc,
isRTL: boolean,
narrow: boolean,
integratedLayout: boolean
) =>
[
{
name: "layout",
selector: {
select: {
mode: "box",
box_max_columns: narrow ? 1 : 3,
options: ["responsive", "start", "center"].map((value) => {
box_max_columns: narrow ? 1 : 4,
options: [
"responsive",
"start",
"center",
VIEW_HEADER_LAYOUT_INTEGRATED,
].map((value) => {
const labelKey =
value === "start" && isRTL ? `${value}_rtl` : value;
return {
@@ -65,8 +82,8 @@ export class HuiViewHeaderSettingsEditor extends LitElement {
`ui.panel.lovelace.editor.edit_view_header.settings.layout_options.${value}_description`
),
image: {
src: `/static/images/form/view_header_layout_${value}.svg`,
src_dark: `/static/images/form/view_header_layout_${value}_dark.svg`,
src: `/static/images/form/view_header_layout_${value === VIEW_HEADER_LAYOUT_INTEGRATED ? "responsive" : value}.svg`,
src_dark: `/static/images/form/view_header_layout_${value === VIEW_HEADER_LAYOUT_INTEGRATED ? "responsive" : value}_dark.svg`,
flip_rtl: true,
},
};
@@ -76,6 +93,7 @@ export class HuiViewHeaderSettingsEditor extends LitElement {
},
{
name: "badges_position",
disabled: integratedLayout,
selector: {
select: {
mode: "box",
@@ -95,6 +113,7 @@ export class HuiViewHeaderSettingsEditor extends LitElement {
},
{
name: "badges_wrap",
disabled: integratedLayout,
selector: {
select: {
mode: "box",
@@ -125,16 +144,27 @@ export class HuiViewHeaderSettingsEditor extends LitElement {
return nothing;
}
const layout = this._selectedLayout;
const integratedLayout = layout === VIEW_HEADER_LAYOUT_INTEGRATED;
const data = {
layout: this.config?.layout || DEFAULT_VIEW_HEADER_LAYOUT,
badges_position:
this.config?.badges_position || DEFAULT_VIEW_HEADER_BADGES_POSITION,
badges_wrap: this.config?.badges_wrap || DEFAULT_VIEW_HEADER_BADGES_WRAP,
layout,
badges_position: integratedLayout
? "top"
: (this.config?.badges_position ?? DEFAULT_VIEW_HEADER_BADGES_POSITION),
badges_wrap: integratedLayout
? "scroll"
: (this.config?.badges_wrap ?? DEFAULT_VIEW_HEADER_BADGES_WRAP),
};
const narrow = this.narrow;
const isRTL = computeRTL(this.hass);
const schema = this._schema(this.hass.localize, isRTL, narrow);
const schema = this._schema(
this.hass.localize,
isRTL,
narrow,
integratedLayout
);
return html`
<ha-form
@@ -147,15 +177,27 @@ export class HuiViewHeaderSettingsEditor extends LitElement {
`;
}
private _valueChanged(ev: CustomEvent): void {
private _valueChanged(ev: ValueChangedEvent<LovelaceViewHeaderConfig>): void {
ev.stopPropagation();
const newData = ev.detail.value as LovelaceViewConfig;
const layout =
ev.detail.value.layout ??
this._selectedLayout ??
DEFAULT_VIEW_HEADER_LAYOUT;
this._selectedLayout = layout;
const integratedLayout = layout === VIEW_HEADER_LAYOUT_INTEGRATED;
const config: LovelaceViewHeaderConfig = {
...this.config,
...newData,
...ev.detail.value,
};
if (integratedLayout) {
config.badges_position = "top";
config.badges_wrap = "scroll";
}
fireEvent(this, "config-changed", { config });
}

View File

@@ -26,6 +26,7 @@ import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
import { DragScrollController } from "../../common/controllers/drag-scroll-controller";
import { fireEvent } from "../../common/dom/fire_event";
import { goBack, navigate } from "../../common/navigate";
import type { LocalizeKeys } from "../../common/translations/localize";
@@ -50,6 +51,7 @@ import "../../components/ha-tab-group";
import "../../components/ha-tab-group-tab";
import "../../components/ha-tooltip";
import { createAreaRegistryEntry } from "../../data/area/area_registry";
import type { LovelaceViewElement } from "../../data/lovelace";
import type {
LovelaceConfig,
LovelaceRawConfig,
@@ -61,6 +63,7 @@ import {
fetchDashboards,
updateDashboard,
} from "../../data/lovelace/dashboard";
import { isStrategyView } from "../../data/lovelace/config/view";
import { fetchLovelaceInfo } from "../../data/lovelace/resource";
import { getPanelTitle } from "../../data/panel";
import { createPerson } from "../../data/person";
@@ -90,12 +93,16 @@ import { showSaveDialog } from "./editor/show-save-config-dialog";
import { showEditViewDialog } from "./editor/view-editor/show-edit-view-dialog";
import { getLovelaceStrategy } from "./strategies/get-strategy";
import { isLegacyStrategyConfig } from "./strategies/legacy-strategy";
import type { HuiBadge } from "./badges/hui-badge";
import type { Lovelace } from "./types";
import "./badges/hui-view-badges";
import "./views/hui-view";
import type { HUIView } from "./views/hui-view";
import "./views/hui-view-background";
import "./views/hui-view-container";
const VIEW_HEADER_LAYOUT_INTEGRATED = "integrated";
interface ActionItem {
icon: string;
key: LocalizeKeys;
@@ -158,6 +165,11 @@ class HUIRoot extends LitElement {
private _viewCache: Record<string, HUIView> = {};
private _toolbarBadgeDragScrollController = new DragScrollController(this, {
selector: ".toolbar-badges.scroll",
enabled: false,
});
private _viewScrollPositions: Record<string, number> = {};
private _restoreScroll = false;
@@ -538,6 +550,16 @@ class HUIRoot extends LitElement {
const isSubview = curViewConfig?.subview;
const hasTabViews = views.filter((view) => !view.subview).length > 1;
const headerConfig =
curViewConfig && !isStrategyView(curViewConfig)
? curViewConfig.header
: undefined;
const showToolbarBadges =
!this._editMode &&
!this.narrow &&
headerConfig?.layout === VIEW_HEADER_LAYOUT_INTEGRATED &&
typeof this._curView === "number";
const toolbarBadges = showToolbarBadges ? this._getCurrentViewBadges() : [];
return html`
<div
@@ -548,7 +570,12 @@ class HUIRoot extends LitElement {
>
<div class="header">
<slot name="toolbar">
<div class="toolbar">
<div
class=${classMap({
toolbar: true,
"integrated-badges": showToolbarBadges,
})}
>
${this._editMode
? html`
<div class="main-title">
@@ -593,6 +620,27 @@ class HUIRoot extends LitElement {
${views[0]?.title ?? dashboardTitle}
</div>
`}
${showToolbarBadges && toolbarBadges.length > 0
? html`
<div
class=${classMap({
"toolbar-badges": true,
scroll: true,
dragging:
this._toolbarBadgeDragScrollController
.scrolling,
})}
>
<hui-view-badges
.badges=${toolbarBadges}
.hass=${this.hass}
.lovelace=${this.lovelace!}
.viewIndex=${this._curView as number}
.showAddLabel=${false}
></hui-view-badges>
</div>
`
: nothing}
<div class="action-items">${this._renderActionItems()}</div>
`}
</div>
@@ -826,6 +874,23 @@ class HUIRoot extends LitElement {
this._selectView(newSelectView);
});
}
const currentViewConfig =
typeof this._curView === "number"
? this.config.views[this._curView]
: undefined;
const currentViewHeaderConfig =
currentViewConfig && !isStrategyView(currentViewConfig)
? currentViewConfig.header
: undefined;
const integratedLayout =
currentViewHeaderConfig?.layout === VIEW_HEADER_LAYOUT_INTEGRATED;
const toolbarBadges = this._getCurrentViewBadges();
this._toolbarBadgeDragScrollController.enabled =
!this._editMode &&
!this.narrow &&
integratedLayout &&
toolbarBadges.length > 0;
}
private get config(): LovelaceConfig {
@@ -844,6 +909,16 @@ class HUIRoot extends LitElement {
return this.shadowRoot!.getElementById("view") as HTMLDivElement;
}
private _getCurrentViewBadges(): HuiBadge[] {
if (typeof this._curView !== "number") {
return [];
}
const view = this._viewCache[this._curView];
const layout = view?.firstElementChild as LovelaceViewElement | null;
return layout?.badges || [];
}
private _handleRefresh = () => {
fireEvent(this, "config-refresh");
};
@@ -1323,7 +1398,7 @@ class HUIRoot extends LitElement {
display: flex;
align-items: center;
font-size: var(--ha-font-size-xl);
padding: 0px 12px;
padding: 0 var(--ha-space-3);
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
}
@@ -1331,7 +1406,7 @@ class HUIRoot extends LitElement {
border-bottom: none;
}
.narrow .toolbar {
padding: 0 4px;
padding: 0 var(--ha-space-1);
}
.main-title {
margin-inline-start: var(--ha-space-6);
@@ -1342,6 +1417,9 @@ class HUIRoot extends LitElement {
white-space: nowrap;
min-width: 0;
}
.toolbar.integrated-badges .main-title {
flex-grow: 0;
}
.narrow .main-title {
margin-inline-start: var(--ha-space-2);
}
@@ -1350,6 +1428,37 @@ class HUIRoot extends LitElement {
display: flex;
align-items: center;
}
.toolbar-badges {
display: flex;
align-items: center;
min-width: 0;
width: auto;
flex: 1 1 0;
max-width: none;
margin-inline-start: var(--ha-space-4);
margin-inline-end: var(--ha-space-1);
}
.toolbar-badges.scroll {
overflow: auto;
scrollbar-color: var(--scrollbar-thumb-color) transparent;
scrollbar-width: none;
mask-image: linear-gradient(
90deg,
transparent 0%,
black 16px,
black calc(100% - 16px),
transparent 100%
);
}
.toolbar-badges.dragging {
pointer-events: none;
}
.toolbar-badges hui-view-badges {
width: 100%;
--badges-wrap: nowrap;
--badges-aligmnent: flex-start;
--badge-padding: var(--ha-space-4);
}
ha-tab-group {
--ha-tab-indicator-color: var(
--app-header-selection-bar-color,

View File

@@ -202,6 +202,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
<hui-view-header
.hass=${this.hass}
.badges=${this.badges}
.narrow=${this.narrow}
.lovelace=${this.lovelace}
.viewIndex=${this.index}
.config=${this._config?.header}

View File

@@ -23,6 +23,7 @@ import { showEditViewHeaderDialog } from "../editor/view-header/show-edit-view-h
import type { Lovelace } from "../types";
export const DEFAULT_VIEW_HEADER_LAYOUT = "center";
export const VIEW_HEADER_LAYOUT_INTEGRATED = "integrated";
export const DEFAULT_VIEW_HEADER_BADGES_POSITION = "bottom";
export const DEFAULT_VIEW_HEADER_BADGES_WRAP = "wrap";
@@ -32,6 +33,8 @@ export class HuiViewHeader extends LitElement {
@property({ attribute: false }) public lovelace!: Lovelace;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public card?: HuiCard;
@property({ attribute: false }) public badges: HuiBadge[] = [];
@@ -202,9 +205,9 @@ export class HuiViewHeader extends LitElement {
this.config?.badges_position ?? DEFAULT_VIEW_HEADER_BADGES_POSITION;
const badgesWrap =
this.config?.badges_wrap ?? DEFAULT_VIEW_HEADER_BADGES_WRAP;
const badgeDragging = this._dragScrollController.scrolling
? "dragging"
: "";
const badgeDragging = this._dragScrollController.scrolling;
const badgesInToolbar =
layout === VIEW_HEADER_LAYOUT_INTEGRATED && !editMode && !this.narrow;
const hasHeading = card !== undefined;
const hasBadges = this.badges.length > 0;
@@ -264,10 +267,17 @@ export class HuiViewHeader extends LitElement {
</div>
`
: nothing}
${this.lovelace && (editMode || this.badges.length > 0)
${this.lovelace &&
!badgesInToolbar &&
(editMode || this.badges.length > 0)
? html`
<div
class="badges ${badgesPosition} ${badgesWrap} ${badgeDragging}"
class=${classMap({
badges: true,
[badgesPosition]: true,
[badgesWrap]: true,
dragging: badgeDragging,
})}
>
<hui-view-badges
.badges=${this.badges}

View File

@@ -8695,7 +8695,9 @@
"start_description": "Always stacked",
"start_rtl": "Right aligned",
"center": "Centered",
"center_description": "Always stacked"
"center_description": "Always stacked",
"integrated": "Integrated into toolbar",
"integrated_description": "Badges are shown in the top app bar on desktop"
},
"badges_position": "Badges position",
"badges_position_options": {