diff --git a/src/components/ha-areas-floors-display-editor.ts b/src/components/ha-areas-floors-display-editor.ts
new file mode 100644
index 0000000000..414e2550c7
--- /dev/null
+++ b/src/components/ha-areas-floors-display-editor.ts
@@ -0,0 +1,219 @@
+import { mdiTextureBox } from "@mdi/js";
+import type { TemplateResult } from "lit";
+import { LitElement, css, html } from "lit";
+import { customElement, property } from "lit/decorators";
+import memoizeOne from "memoize-one";
+import { fireEvent } from "../common/dom/fire_event";
+import { computeFloorName } from "../common/entity/compute_floor_name";
+import { getAreaContext } from "../common/entity/context/get_area_context";
+import { stringCompare } from "../common/string/compare";
+import { areaCompare } from "../data/area_registry";
+import type { FloorRegistryEntry } from "../data/floor_registry";
+import type { HomeAssistant } from "../types";
+import "./ha-expansion-panel";
+import "./ha-floor-icon";
+import "./ha-items-display-editor";
+import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
+import "./ha-svg-icon";
+import "./ha-textfield";
+
+export interface AreasDisplayValue {
+ hidden?: string[];
+ order?: string[];
+}
+
+const UNASSIGNED_FLOOR = "__unassigned__";
+
+@customElement("ha-areas-floors-display-editor")
+export class HaAreasFloorsDisplayEditor extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property() public label?: string;
+
+ @property({ attribute: false }) public value?: AreasDisplayValue;
+
+ @property() public helper?: string;
+
+ @property({ type: Boolean }) public expanded = false;
+
+ @property({ type: Boolean }) public disabled = false;
+
+ @property({ type: Boolean }) public required = false;
+
+ @property({ type: Boolean, attribute: "show-navigation-button" })
+ public showNavigationButton = false;
+
+ protected render(): TemplateResult {
+ const groupedItems = this._groupedItems(this.hass.areas, this.hass.floors);
+
+ const filteredFloors = this._sortedFloors(this.hass.floors).filter(
+ (floor) =>
+ // Only include floors that have areas assigned to them
+ groupedItems[floor.floor_id]?.length > 0
+ );
+
+ const value: DisplayValue = {
+ order: this.value?.order ?? [],
+ hidden: this.value?.hidden ?? [],
+ };
+
+ return html`
+
+
+ ${filteredFloors.map(
+ (floor) => html`
+
+ `
+ )}
+
+ `;
+ }
+
+ private _groupedItems = memoizeOne(
+ (
+ hassAreas: HomeAssistant["areas"],
+ // update items if floors change
+ _hassFloors: HomeAssistant["floors"]
+ ): Record => {
+ const compare = areaCompare(hassAreas);
+
+ const areas = Object.values(hassAreas).sort((areaA, areaB) =>
+ compare(areaA.area_id, areaB.area_id)
+ );
+ const groupedItems: Record = areas.reduce(
+ (acc, area) => {
+ const { floor } = getAreaContext(area, this.hass!);
+ const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
+
+ if (!acc[floorId]) {
+ acc[floorId] = [];
+ }
+ acc[floorId].push({
+ value: area.area_id,
+ label: area.name,
+ icon: area.icon ?? undefined,
+ iconPath: mdiTextureBox,
+ description: floor?.name,
+ });
+
+ return acc;
+ },
+ {} as Record
+ );
+ return groupedItems;
+ }
+ );
+
+ private _sortedFloors = memoizeOne(
+ (hassFloors: HomeAssistant["floors"]): FloorRegistryEntry[] => {
+ const floors = Object.values(hassFloors).sort((floorA, floorB) => {
+ if (floorA.level !== floorB.level) {
+ return (floorA.level ?? 0) - (floorB.level ?? 0);
+ }
+ return stringCompare(floorA.name, floorB.name);
+ });
+ floors.push({
+ floor_id: UNASSIGNED_FLOOR,
+ name: this.hass.localize(
+ "ui.panel.lovelace.strategy.areas.unassigned_areas"
+ ),
+ icon: null,
+ level: 999999,
+ aliases: [],
+ created_at: 0,
+ modified_at: 0,
+ });
+ return floors;
+ }
+ );
+
+ private async _areaDisplayChanged(ev) {
+ ev.stopPropagation();
+ const value = ev.detail.value as DisplayValue;
+ const currentFloorId = ev.currentTarget.floorId;
+
+ const floorIds = this._sortedFloors(this.hass.floors).map(
+ (floor) => floor.floor_id
+ );
+
+ const newHidden: string[] = [];
+ const newOrder: string[] = [];
+
+ for (const floorId of floorIds) {
+ if (currentFloorId === floorId) {
+ newHidden.push(...(value.hidden ?? []));
+ newOrder.push(...(value.order ?? []));
+ continue;
+ }
+ const hidden = this.value?.hidden?.filter(
+ (areaId) => this.hass.areas[areaId]?.floor_id === floorId
+ );
+ if (hidden) {
+ newHidden.push(...hidden);
+ }
+ const order = this.value?.order?.filter(
+ (areaId) => this.hass.areas[areaId]?.floor_id === floorId
+ );
+ if (order) {
+ newOrder.push(...order);
+ }
+ }
+
+ const newValue: AreasDisplayValue = {
+ hidden: newHidden,
+ order: newOrder,
+ };
+ if (newValue.hidden?.length === 0) {
+ delete newValue.hidden;
+ }
+ if (newValue.order?.length === 0) {
+ delete newValue.order;
+ }
+
+ fireEvent(this, "value-changed", { value: newValue });
+ }
+
+ static styles = css`
+ .floor .header p {
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ flex: 1:
+ }
+ .floor .header {
+ margin: 16px 0 8px 0;
+ padding: 0 8px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-areas-floors-display-editor": HaAreasFloorsDisplayEditor;
+ }
+}
diff --git a/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts b/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts
index 74bbd0a38c..230b2230fe 100644
--- a/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts
+++ b/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts
@@ -11,6 +11,8 @@ import type { AreaCardConfig, HeadingCardConfig } from "../../cards/types";
import type { EntitiesDisplay } from "./area-view-strategy";
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helper";
+const UNASSIGNED_FLOOR = "__unassigned__";
+
interface AreaOptions {
groups_options?: Record;
}
@@ -46,13 +48,20 @@ export class AreasOverviewViewStrategy extends ReactiveElement {
const floorSections = [
...floors,
- { floor_id: "default", name: "Default", level: null, icon: null },
+ {
+ floor_id: UNASSIGNED_FLOOR,
+ name: hass.localize(
+ "ui.panel.lovelace.strategy.areas.unassigned_areas"
+ ),
+ level: null,
+ icon: null,
+ },
]
.map((floor) => {
const areasInFloors = areas.filter(
(area) =>
area.floor_id === floor.floor_id ||
- (!area.floor_id && floor.floor_id === "default")
+ (!area.floor_id && floor.floor_id === UNASSIGNED_FLOOR)
);
if (areasInFloors.length === 0) {
diff --git a/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts b/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts
index 6edf10b22c..311641caa2 100644
--- a/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts
+++ b/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts
@@ -22,6 +22,7 @@ import {
type AreaRegistryEntry,
} from "../../../../../data/area_registry";
import { buttonLinkStyle } from "../../../../../resources/styles";
+import "../../../../../components/ha-areas-floors-display-editor";
@customElement("hui-areas-dashboard-strategy-editor")
export class HuiAreasDashboardStrategyEditor
@@ -122,7 +123,7 @@ export class HuiAreasDashboardStrategyEditor
const value = this._config.areas_display;
return html`
-
+ >
`;
}
diff --git a/src/translations/en.json b/src/translations/en.json
index 3632d746f5..009e1806c7 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -6625,7 +6625,8 @@
"security": "Security",
"actions": "Actions",
"others": "Others"
- }
+ },
+ "unassigned_areas": "[%key:ui::panel::config::areas::picker::unassigned_areas%]"
}
},
"cards": {