Compare commits

...
Author SHA1 Message Date
Bram KragtenandClaude Opus 5.5 30467c3a51 Add a bottom bar height variable for bottom offsets
Introduce --ha-bottom-bar-height and express the bottom offsets of subpage
content, FABs, data table empty rows and toasts with it. Remove the unused
#fab[is-wide] and #fab.tabs selectors. No visible change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 12:37:38 +02:00
Bram KragtenandClaude Opus 5.5 fc760b9325 Move PageNavigation type to its own data module
PageNavigation is used far beyond the tabs subpage layout, so it now lives in
src/data/page_navigation.ts. No behavior change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 12:37:38 +02:00
28 changed files with 112 additions and 80 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../data/page_navigation";
import type { HomeAssistant, HomeAssistantConfig } from "../../types";
import { ensureArray } from "../array/ensure-array";
import { isComponentLoaded } from "./is_component_loaded";
+1 -1
View File
@@ -1,4 +1,4 @@
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../data/page_navigation";
import type { HomeAssistant } from "../../types";
import { canShowPage } from "./can_show_page";
+1 -1
View File
@@ -5,7 +5,7 @@ import "./ha-icon-next";
import "./ha-svg-icon";
import "./item/ha-list-item-button";
import "./list/ha-list-nav";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../data/page_navigation";
import type { HomeAssistant } from "../types";
@customElement("ha-config-navigation-list")
+5 -2
View File
@@ -232,8 +232,11 @@ export class HaToast extends LitElement {
inset-block-start: auto;
inset-inline-end: auto;
inset-block-end: calc(
var(--safe-area-inset-bottom, 0px) + var(--ha-space-4) +
var(--ha-toast-bottom-offset, 0px)
max(
var(--safe-area-inset-bottom, 0px),
var(--ha-bottom-bar-height, 0px)
) +
var(--ha-space-4) + var(--ha-toast-bottom-offset, 0px)
);
inset-inline-start: 50%;
margin: 0;
+20
View File
@@ -0,0 +1,20 @@
import type { HomeAssistant } from "../types";
export interface PageNavigation {
path: string;
translationKey?: string;
component?: string | string[];
name?: string;
core?: boolean;
/** Hide from non-admin users in filtered navigation and quick bar. */
adminOnly?: boolean;
iconPath?: string;
iconSecondaryPath?: string;
iconViewBox?: string;
description?: string;
iconColor?: string;
// Shown next to the name of the tab
badge?: string;
info?: any;
filter?: (hass: Pick<HomeAssistant, "entities">) => boolean;
}
+1 -1
View File
@@ -9,7 +9,7 @@ import {
mdiPlayBoxMultiple,
} from "@mdi/js";
import type { LocalizeKeys } from "../common/translations/localize";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { PageNavigation } from "./page_navigation";
import type { HomeAssistant, PanelInfo } from "../types";
export const APP_PANEL = "app";
+1 -1
View File
@@ -12,7 +12,7 @@ import {
} from "../common/config/filter_navigation_pages";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { PageNavigation } from "./page_navigation";
import { configSections } from "../panels/config/config-sections";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types";
+18 -12
View File
@@ -155,9 +155,18 @@ class HassSubpage extends LitElement {
1px - var(--header-height, 0px) - var(
--safe-area-inset-top,
0px
) - var(--safe-area-inset-bottom, 0px)
) - max(
var(--safe-area-inset-bottom, 0px),
var(--ha-bottom-bar-height, 0px)
)
);
padding-bottom: max(
0px,
var(--safe-area-inset-bottom, 0px) - var(
--ha-bottom-bar-height,
0px
)
);
padding-bottom: var(--safe-area-inset-bottom, 0px);
margin-right: var(--safe-area-inset-right);
overflow-y: auto;
overflow: auto;
@@ -184,7 +193,13 @@ class HassSubpage extends LitElement {
right: calc(16px + var(--safe-area-inset-right, 0px));
inset-inline-end: calc(16px + var(--safe-area-inset-right, 0px));
inset-inline-start: initial;
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
bottom: calc(
16px +
max(
var(--safe-area-inset-bottom, 0px),
var(--ha-bottom-bar-height, 0px)
)
);
z-index: 1;
display: flex;
flex-wrap: wrap;
@@ -192,15 +207,6 @@ class HassSubpage extends LitElement {
gap: var(--ha-space-2);
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
:host([narrow]) #fab.tabs {
bottom: calc(84px + var(--safe-area-inset-bottom, 0px));
}
#fab[is-wide] {
bottom: calc(24px + var(--safe-area-inset-bottom, 0px));
right: calc(24px + var(--safe-area-inset-right, 0px));
inset-inline-end: calc(24px + var(--safe-area-inset-right, 0px));
inset-inline-start: initial;
}
`,
];
}
+15 -9
View File
@@ -46,10 +46,10 @@ import {
entitiesContext,
internationalizationContext,
} from "../data/context";
import type { PageNavigation } from "../data/page_navigation";
import { KeyboardShortcutMixin } from "../mixins/keyboard-shortcut-mixin";
import type { HomeAssistant, Route } from "../types";
import "./hass-tabs-subpage";
import type { PageNavigation } from "./hass-tabs-subpage";
@customElement("hass-tabs-subpage-data-table")
export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
@@ -786,7 +786,10 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
width: 100%;
height: 100%;
--data-table-border-width: 0;
--data-table-empty-row-height: var(--safe-area-inset-bottom, 0px);
--data-table-empty-row-height: max(
0px,
var(--safe-area-inset-bottom, 0px) - var(--ha-bottom-bar-height, 0px)
);
/* Same insets as the content padding of hass-tabs-subpage */
--data-table-inset-right: var(--safe-area-inset-right, 0px);
}
@@ -805,17 +808,20 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
display: block;
}
/* Last content row should keep the same padding above the fab as the fab
has to the bottom (16px standard fab bottom padding) + the safe-area inset. */
has to the bottom (16px standard fab bottom padding) + the safe-area inset
not already covered by a bottom bar. */
:host([has-fab]) ha-data-table {
--data-table-empty-row-height: calc(
48px + 16px * 2 + var(--safe-area-inset-bottom, 0px)
48px + 16px * 2 +
max(
0px,
var(--safe-area-inset-bottom, 0px) - var(
--ha-bottom-bar-height,
0px
)
)
);
}
/* In narrow view with tabs shown at the bottom, the tab bar already
accounts for safe-area-inset-bottom. No extra empty-row height is needed. */
:host([narrow][show-tabs]:not([has-fab])) ha-data-table {
--data-table-empty-row-height: 0px;
}
/* Reserve space for fab + doubled narrow-mode bottom padding (28px * 2)
when using narrow layout with bottom tabs. */
:host([narrow][show-tabs][has-fab]) ha-data-table {
+25 -34
View File
@@ -29,6 +29,7 @@ import {
internationalizationContext,
narrowViewportContext,
} from "../data/context";
import type { PageNavigation } from "../data/page_navigation";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, Route } from "../types";
@@ -37,25 +38,6 @@ const normalizePathname = (pathname: string): string =>
? pathname.slice(0, -1)
: pathname;
export interface PageNavigation {
path: string;
translationKey?: string;
component?: string | string[];
name?: string;
core?: boolean;
/** Hide from non-admin users in filtered navigation and quick bar. */
adminOnly?: boolean;
iconPath?: string;
iconSecondaryPath?: string;
iconViewBox?: string;
description?: string;
iconColor?: string;
// Shown next to the name of the tab
badge?: string;
info?: any;
filter?: (hass: Pick<HomeAssistant, "entities">) => boolean;
}
@customElement("hass-tabs-subpage")
export class HassTabsSubpage extends LitElement {
// Unread, kept for callers that still pass it until they move to contexts
@@ -316,6 +298,11 @@ export class HassTabsSubpage extends LitElement {
width: 100%;
position: fixed;
}
:host([narrow][show-tabs]) {
--ha-bottom-bar-height: calc(
var(--header-height, 0px) + var(--safe-area-inset-bottom, 0px)
);
}
.container {
display: flex;
@@ -427,19 +414,23 @@ export class HassTabsSubpage extends LitElement {
padding-right: var(--safe-area-inset-right);
overflow: auto;
-webkit-overflow-scrolling: touch;
margin-bottom: var(--ha-bottom-bar-height, 0px);
}
:host([narrow]) .content {
padding-left: var(--safe-area-inset-left);
}
:host([narrow][show-tabs]) .content {
/* Bottom bar reuses header height */
margin-bottom: calc(
var(--header-height, 0px) + var(--safe-area-inset-bottom, 0px)
);
}
.content .fab-bottom-space {
height: calc(64px + var(--safe-area-inset-bottom, 0px));
height: calc(
64px +
max(
0px,
var(--safe-area-inset-bottom, 0px) - var(
--ha-bottom-bar-height,
0px
)
)
);
}
:host([narrow][show-tabs]) .content .fab-bottom-space {
@@ -451,7 +442,13 @@ export class HassTabsSubpage extends LitElement {
right: calc(16px + var(--safe-area-inset-right, 0px));
inset-inline-end: calc(16px + var(--safe-area-inset-right));
inset-inline-start: initial;
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
bottom: calc(
16px +
max(
var(--safe-area-inset-bottom, 0px),
var(--ha-bottom-bar-height, 0px)
)
);
z-index: 1;
display: flex;
flex-wrap: wrap;
@@ -460,13 +457,7 @@ export class HassTabsSubpage extends LitElement {
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
:host([narrow][show-tabs]) #fab {
bottom: calc(84px + var(--safe-area-inset-bottom, 0px));
}
#fab[is-wide] {
bottom: 24px;
right: 24px;
inset-inline-end: 24px;
inset-inline-start: initial;
bottom: calc(28px + var(--ha-bottom-bar-height, 0px));
}
.pane {
+5 -2
View File
@@ -291,8 +291,11 @@ class NotificationManager extends LitElement {
inset-block-start: auto;
inset-inline-end: auto;
inset-block-end: calc(
var(--safe-area-inset-bottom, 0px) + var(--ha-space-4) +
var(--notification-stack-bottom-offset, 0px)
max(
var(--safe-area-inset-bottom, 0px),
var(--ha-bottom-bar-height, 0px)
) +
var(--ha-space-4) + var(--notification-stack-bottom-offset, 0px)
);
inset-inline-start: 50%;
margin: 0;
@@ -22,7 +22,7 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../data/page_navigation";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
+1 -1
View File
@@ -41,7 +41,7 @@ import {
mdiZWave,
} from "@mdi/js";
import memoizeOne from "memoize-one";
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../data/page_navigation";
import type { HomeAssistant } from "../../types";
import { mdiMqttLogo } from "../../resources/mqtt-logo-svg";
@@ -36,7 +36,7 @@ import {
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../data/page_navigation";
import { ChildPanelReady } from "../../../layouts/panel-ready";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
@@ -8,7 +8,7 @@ import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import type { CloudStatus } from "../../../data/cloud";
import { getConfigEntries } from "../../../data/config_entries";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../data/page_navigation";
import {
childPanelReadyContext,
type RegisterChildPanelReady,
+1 -1
View File
@@ -38,7 +38,7 @@ import "./components/ha-energy-gas-settings";
import "./components/ha-energy-water-settings";
import { fileDownload } from "../../../util/file_download";
import { showToast } from "../../../util/toast";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../data/page_navigation";
import { showEnergyCustomiseDialog } from "./dialogs/show-dialog-energy-customise";
const INITIAL_CONFIG: EnergyPreferences = {
@@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import type { RouterOptions } from "../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../layouts/hass-router-page";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../data/page_navigation";
import type { HomeAssistant } from "../../../types";
export const hardwareTabs = (hass: HomeAssistant): PageNavigation[] => {
@@ -24,7 +24,7 @@ import {
} from "../../../../../data/bluetooth";
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../data/page_navigation";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { showBluetoothDeviceInfoDialog } from "./show-dialog-bluetooth-device-info";
@@ -21,7 +21,7 @@ import {
} from "../../../../../data/bluetooth";
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../data/page_navigation";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
@@ -13,7 +13,7 @@ import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-relative-time";
import type { InfraredDevice } from "../../../../../data/infrared";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../data/page_navigation";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
@@ -17,7 +17,7 @@ import {
type RadioFrequencyTransmitter,
} from "../../../../../data/radio_frequency";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../data/page_navigation";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
@@ -19,7 +19,7 @@ import { fetchZHADevice } from "../../../../../data/zha";
import "../../../../../layouts/hass-error-screen";
import "../../../../../layouts/hass-subpage";
import "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../data/page_navigation";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "./device-page/zha-device-bindings-pane";
@@ -14,7 +14,7 @@ import "../../../../../components/ha-button";
import "../../../../../components/ha-icon-button";
import type { ZHAGroup } from "../../../../../data/zha";
import { fetchGroups } from "../../../../../data/zha";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../data/page_navigation";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
@@ -44,7 +44,7 @@ import type { HaIconButton } from "../../../components/ha-icon-button";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-svg-icon";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../data/page_navigation";
import type { HomeAssistantUI, Route } from "../../../types";
import { showMarketplaceAddFromLink } from "../tools/add-from-link";
import "../components/ha-marketplace-discover";
+1 -1
View File
@@ -12,7 +12,7 @@ import "./ha-pick-theme-row";
import { isExternal } from "../../data/external";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import "../../layouts/hass-subpage";
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../data/page_navigation";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types";
import { showEditProfileDialog } from "./show-dialog-edit-profile";
+3
View File
@@ -8,6 +8,9 @@ export const mainStyles = css`
/* for header */
--header-height: 56px;
/* Space taken by bottom chrome, including the safe-area inset it covers */
--ha-bottom-bar-height: 0px;
/* opacity for dark text on a light background */
--dark-divider-opacity: 0.12;
--dark-disabled-opacity: 0.38; /* or hint text or icon */
+1 -1
View File
@@ -4,7 +4,7 @@ import {
isLoadedIntegration,
isCore,
} from "../../../src/common/config/can_show_page";
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../src/data/page_navigation";
import type { HomeAssistant } from "../../../src/types";
describe("canShowPage", () => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { getConfigSubpageTitle } from "../../src/data/panel";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../src/data/page_navigation";
import type { HomeAssistant } from "../../src/types";
const translations: Record<string, string> = {