mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
fix(ui): measure the mobile nav instead of guessing its height
The Assistant overlay and its backdrop reserved a hardcoded calc(5rem + env(safe-area-inset-bottom)) for a bar that measures 45px. At 375x812 the backdrop ended at y=732 while the bar starts at 767, leaving a 35px band that was neither dimmed nor click-blocked: page content showed through and stayed interactive outside an open modal. The bar's height is content-driven and already includes the safe-area inset via pb-safe, so no caller can hardcode it correctly. Five sites had tried: the Assistant panel, its backdrop, the star banner, and both halves of the .filter-bottom-nav-aware-panel rule, which also carried a derived 6rem. MobileNavBar now publishes its measured height as --pulse-mobile-nav-height and those five sites read it. Publishing happens in onMount rather than the ref, because Solid runs refs before the node is in the document where offsetHeight is 0; reading after mount forces layout and gets the real value on first paint. A ResizeObserver covers content-driven changes and a resize listener covers the xl breakpoint, where the bar swaps between its height and 0 by display alone. The declared :root value is only a pre-measurement fallback and deliberately under-estimates. Over-reserving is what produces the un-dimmed band; reserving slightly less is harmless because the opaque bar paints over it. Verified at 375x812 and 1280x800: backdrop and panel now both end at 767 against a bar top of 767, gap 0; the point 5px above the bar hit-tests to the Assistant panel rather than page content; the filter panel resolves to 45px on mobile; and the star banner's md:bottom-4 still wins at 16px. Adds the regression proofs the shape guard asks for, including an architecture test that fails if any runtime source reintroduces a literal bar height, and records the ownership boundary in the frontend-primitives and ai-runtime contracts.
This commit is contained in:
@@ -7659,3 +7659,13 @@ then owns deduplication and persistence. A submitted ID that identifies any
|
||||
real out-of-scope resource, an ambiguous name, a type mismatch, or a name that
|
||||
does not resolve inside the lifecycle scope remains rejected. No fuzzy name or
|
||||
ID matching is permitted at this boundary.
|
||||
|
||||
The Assistant overlay clears the mobile navigation bar from the published bar
|
||||
height rather than a literal of its own. In overlay layout both the panel in
|
||||
`frontend-modern/src/components/AI/Chat/index.tsx` and its closing backdrop end
|
||||
at `--pulse-mobile-nav-height`, so the dimmed, click-blocking backdrop reaches
|
||||
the top edge of the bar with no gap. Reserving more than the bar's real height
|
||||
leaves page content visible and interactive below the backdrop while the
|
||||
Assistant is open, which is a modal-containment defect rather than a cosmetic
|
||||
one. The bar owns that value; see the frontend-primitives contract. Proofs live
|
||||
in `frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx`.
|
||||
|
||||
@@ -6185,3 +6185,32 @@ alert surfaces use the same typed monitoring and lifecycle vocabulary and
|
||||
provider-ownership presentation helper. They must not create local alert mute,
|
||||
archive, or removal state. The menu remains keyboard reachable, uses ordinary
|
||||
shared surface and border tokens, and retains usable controls at phone width.
|
||||
|
||||
### The mobile navigation bar publishes its own height
|
||||
|
||||
`frontend-modern/src/components/shared/MobileNavBar.tsx` is the only owner of
|
||||
the bottom navigation bar's height. That height is content-driven and already
|
||||
includes the safe-area inset through `pb-safe`, so no consumer may derive it.
|
||||
The bar measures itself after mount, publishes the result as the
|
||||
`--pulse-mobile-nav-height` custom property on the document element, keeps it
|
||||
current through a resize observer and a window resize listener, and removes it
|
||||
when the bar unmounts. Measurement happens in `onMount` rather than in the
|
||||
element ref, because a ref runs before the node is in the document, where the
|
||||
measured height is zero.
|
||||
|
||||
Every surface that must sit on top of the bar reads that property: the
|
||||
Assistant overlay panel and its backdrop, the global GitHub star banner, and
|
||||
the `.filter-bottom-nav-aware-panel` rule in
|
||||
`frontend-modern/src/index.css`. Consumers must not add
|
||||
`env(safe-area-inset-bottom)` on top of the published value, and must not
|
||||
reintroduce a literal bar height. The declared `:root` value is a
|
||||
pre-measurement fallback only and deliberately under-estimates: reserving more
|
||||
than the bar's real height leaves a band between an overlay's backdrop and the
|
||||
bar that is neither dimmed nor click-blocked, while reserving slightly less is
|
||||
covered by the opaque bar. Surfaces with their own placement at wider
|
||||
viewports, such as the star banner's `md:bottom-4`, keep it.
|
||||
|
||||
Proofs live in `frontend-modern/src/components/shared/__tests__/MobileNavBar.test.tsx`,
|
||||
`frontend-modern/src/components/__tests__/GitHubStarBanner.test.tsx`, and
|
||||
`frontend-modern/src/__tests__/App.architecture.test.ts`, which fails if any
|
||||
runtime source reintroduces a literal bar height.
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "4de1ede6030836d09c1e0a118c26c6413fd9f112",
|
||||
"verified_at": "2026-08-19T18:37:46Z",
|
||||
"base_sha": "66c87fb6e8595af47c360573bb8efa51d46492d1",
|
||||
"verified_at": "2026-08-19T18:57:18Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/Workloads/WorkloadsFilter.tsx",
|
||||
"frontend-modern/src/components/Workloads/WorkloadsInventorySummary.tsx",
|
||||
"frontend-modern/src/components/Workloads/workloadsFilterModel.ts",
|
||||
"frontend-modern/src/components/shared/FilterBar/FilterBar.tsx",
|
||||
"frontend-modern/src/components/shared/FilterBar/filterCatalog.ts",
|
||||
"frontend-modern/src/components/shared/FilterButtonGroup.tsx",
|
||||
"frontend-modern/src/components/shared/filterButtonGroupModel.ts",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx"
|
||||
"frontend-modern/src/components/AI/Chat/index.tsx",
|
||||
"frontend-modern/src/components/GitHubStarBanner.tsx",
|
||||
"frontend-modern/src/components/shared/MobileNavBar.tsx",
|
||||
"frontend-modern/src/index.css"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/Workloads/WorkloadsFilter.tsx": "05dc8ff16997ab1f7663a13b9df71ab8219607466933a46e5f2009f96ec80562",
|
||||
"frontend-modern/src/components/Workloads/WorkloadsInventorySummary.tsx": "deleted",
|
||||
"frontend-modern/src/components/Workloads/workloadsFilterModel.ts": "d2b4e7e74c81ba69e7f9c4003568108f02c0dc8bc9071555a1a46bddb4af3908",
|
||||
"frontend-modern/src/components/shared/FilterBar/FilterBar.tsx": "98f8573ace4b7bf5a8ad5bdf6b33e0b6a75ebaabff83f6cd947c233b5a8b9843",
|
||||
"frontend-modern/src/components/shared/FilterBar/filterCatalog.ts": "b3267ab57bcdf6ba7735168608772785a8445e54ccc2c6746c5ea9ff011f98c8",
|
||||
"frontend-modern/src/components/shared/FilterButtonGroup.tsx": "7661c28b8abbc2c01c37a3e96175753df7276e8d3b67a1ae8f283f8563dc787d",
|
||||
"frontend-modern/src/components/shared/filterButtonGroupModel.ts": "06576097404a56f66d6ae0ef23fc913dbff11ceb5833140d00df58ad9711fe41",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "29d2892a9b16e926cdafc35372b2ba7f7f8fe38b8099f7bb51d92319a4995f09",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "2fd5c8176ea82f7c39dfe1fab8c8500001fd48c37b0fa17118d9febf3853ff0f"
|
||||
"frontend-modern/src/components/AI/Chat/index.tsx": "67d3dc20ea1100c3047f27052c35aaa4b1e15a8d2ebeda1022fecae9c772835d",
|
||||
"frontend-modern/src/components/GitHubStarBanner.tsx": "ea65775ad40dc08d54d6e08f6f3c736c545917c11e8e7fbddb71d856afc11d79",
|
||||
"frontend-modern/src/components/shared/MobileNavBar.tsx": "16a6917b7fdc25d77bd9cd6760f132f08a0f13f833ede6d278237e37378549e6",
|
||||
"frontend-modern/src/index.css": "1b195e474bdf5cfa33af971d1a01ed6b7e7e58eb998d823616a292c0ecc6fb1f"
|
||||
},
|
||||
"routes": ["/proxmox/overview", "/vmware/overview"],
|
||||
"routes": [
|
||||
"/",
|
||||
"/proxmox"
|
||||
],
|
||||
"viewports": [
|
||||
{ "width": 1440, "height": 900 },
|
||||
{ "width": 390, "height": 844 }
|
||||
{
|
||||
"width": 375,
|
||||
"height": 812
|
||||
},
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 800
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Proxmox type and status totals shown as centred 11px tabular numbers beside their 12px labels",
|
||||
"Proxmox Nodes total and cluster/standalone topology attached to the existing table heading",
|
||||
"Inventory totals hidden from both filter controls and the Nodes heading through one shared preference, then restored",
|
||||
"Selected Attention filter with inline count and clear-filter action",
|
||||
"Phone filter collapsed and expanded at 390px with inline totals and no horizontal page overflow",
|
||||
"vSphere workload status totals rendered through the same shared filter option-count contract"
|
||||
"authenticated mobile 375x812, bottom navigation mounted and measuring 45px, --pulse-mobile-nav-height published as 45px",
|
||||
"authenticated mobile with the Assistant overlay open, backdrop and panel both ending at the bar's top edge",
|
||||
"authenticated desktop 1280x800, bar display:none and the variable published as 0px",
|
||||
"re-verified after rebase onto 66c87fb6e, which reworked the FilterBar and Proxmox estate surfaces on the same route"
|
||||
],
|
||||
"interactions": [
|
||||
"Opened View, selected Hide for Inventory totals, confirmed workload and node/topology totals disappeared immediately, then restored Show",
|
||||
"Expanded the phone Filters control and inspected both count-bearing segmented controls",
|
||||
"Navigated from Proxmox to vSphere and confirmed shared workload totals beside provider-specific status labels"
|
||||
"opened the Assistant overlay and measured backdrop bottom 767 against bar top 767, gap 0px, previously a 35px un-dimmed clickable band",
|
||||
"hit-tested 5px above the bar and found the Assistant panel covering it rather than page content",
|
||||
"probed .filter-bottom-nav-aware-panel computed bottom, resolving to 45px on mobile from the shared variable",
|
||||
"probed the star banner class combination at 800px and confirmed md:bottom-4 still wins at 16px",
|
||||
"dispatched a resize event and confirmed the published value republishes from 0px to 45px",
|
||||
"reloaded at 1280x800 and confirmed the bar is display:none with the variable published as 0px"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -845,3 +845,46 @@ describe('App architecture', () => {
|
||||
expect(appRuntimeStateSource).not.toContain('/api/license/entitlements');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mobile bottom navigation clearance', () => {
|
||||
const NAV_HEIGHT_VARIABLE = '--pulse-mobile-nav-height';
|
||||
// Matches the shape every drifted copy used, spaced or not. The lookbehind
|
||||
// keeps the declared 2.5rem fallback from matching its own guard.
|
||||
const HARDCODED_BAR_HEIGHT = /(?<![\d.])5rem\s*\+\s*env\(\s*safe-area-inset-bottom/;
|
||||
|
||||
function collectFrontendSources(dir: string): Array<{ path: string; source: string }> {
|
||||
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||
const entryPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return collectFrontendSources(entryPath);
|
||||
}
|
||||
// Only runtime sources matter; a test may quote the old shape in order
|
||||
// to assert against it, as this one does.
|
||||
if (!/\.(css|ts|tsx)$/.test(entry.name)) {
|
||||
return [];
|
||||
}
|
||||
if (entryPath.includes('__tests__') || /\.(test|spec)\./.test(entry.name)) {
|
||||
return [];
|
||||
}
|
||||
return [{ path: entryPath, source: readFileSync(entryPath, 'utf8') }];
|
||||
});
|
||||
}
|
||||
|
||||
it('declares the bar height as a single custom property', () => {
|
||||
expect(appStylesSource).toContain(NAV_HEIGHT_VARIABLE);
|
||||
});
|
||||
|
||||
it('keeps every consumer reading that property instead of its own copy', () => {
|
||||
// The Assistant panel, its backdrop, the star banner and both halves of
|
||||
// the .filter-bottom-nav-aware-panel rule each carried their own
|
||||
// calc(5rem + env(safe-area-inset-bottom)) for a bar that measures ~45px.
|
||||
// The overshoot left a band below the Assistant backdrop that was neither
|
||||
// dimmed nor click-blocked, so page content stayed interactive outside an
|
||||
// open modal. Read the published height rather than adding a sixth copy.
|
||||
const offenders = collectFrontendSources(join(process.cwd(), 'src'))
|
||||
.filter(({ source }) => HARDCODED_BAR_HEIGHT.test(source))
|
||||
.map(({ path }) => path.slice(process.cwd().length + 1));
|
||||
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2003,12 +2003,12 @@ describe('AIChat', () => {
|
||||
const panel = screen.getByText('Pulse Assistant').closest('[data-layout-mode]');
|
||||
expect(panel).toHaveAttribute('data-layout-mode', 'overlay');
|
||||
expect(panel).toHaveClass('top-0');
|
||||
expect(panel).toHaveClass('bottom-[calc(5rem+env(safe-area-inset-bottom,0px))]');
|
||||
expect(panel).toHaveClass('bottom-[var(--pulse-mobile-nav-height)]');
|
||||
expect(panel).toHaveClass('h-auto');
|
||||
expect(panel).not.toHaveClass('inset-y-0');
|
||||
expect(panel).not.toHaveClass('h-full');
|
||||
const backdrop = screen.getByRole('button', { name: 'Close Pulse Assistant backdrop' });
|
||||
expect(backdrop).toHaveClass('bottom-[calc(5rem+env(safe-area-inset-bottom,0px))]');
|
||||
expect(backdrop).toHaveClass('bottom-[var(--pulse-mobile-nav-height)]');
|
||||
expect(backdrop).not.toHaveClass('inset-0');
|
||||
expect(screen.queryByTitle('Collapse Pulse Assistant')).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Close Pulse Assistant')).toBeInTheDocument();
|
||||
|
||||
@@ -2265,7 +2265,7 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
onCleanup(stopPanelResize);
|
||||
const rootClassName = createMemo(() => {
|
||||
if (isOverlayLayout()) {
|
||||
return `fixed top-0 right-0 bottom-[calc(5rem+env(safe-area-inset-bottom,0px))] z-50 flex h-auto w-full flex-col bg-surface transition-transform duration-300 sm:w-[560px] sm:max-w-[calc(100vw-1rem)] ${
|
||||
return `fixed top-0 right-0 bottom-[var(--pulse-mobile-nav-height)] z-50 flex h-auto w-full flex-col bg-surface transition-transform duration-300 sm:w-[560px] sm:max-w-[calc(100vw-1rem)] ${
|
||||
isOpen()
|
||||
? 'translate-x-0 overflow-visible border-l border-border shadow-2xl'
|
||||
: 'translate-x-full overflow-hidden border-l-0'
|
||||
@@ -4248,7 +4248,7 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
<Show when={isOpen() && isOverlayLayout()}>
|
||||
<button
|
||||
type="button"
|
||||
class="fixed inset-x-0 top-0 bottom-[calc(5rem+env(safe-area-inset-bottom,0px))] z-40 bg-slate-950/45 backdrop-blur-[1px]"
|
||||
class="fixed inset-x-0 top-0 bottom-[var(--pulse-mobile-nav-height)] z-40 bg-slate-950/45 backdrop-blur-[1px]"
|
||||
onClick={props.onClose}
|
||||
aria-label="Close Pulse Assistant backdrop"
|
||||
/>
|
||||
|
||||
@@ -116,7 +116,7 @@ export function GitHubStarBanner() {
|
||||
return (
|
||||
<Show when={showModal()}>
|
||||
<section
|
||||
class="fixed left-4 right-20 bottom-[calc(5rem+env(safe-area-inset-bottom,0px))] z-30 max-w-md overflow-hidden rounded-lg border border-border bg-surface text-base-content shadow-xl md:right-auto md:bottom-4"
|
||||
class="fixed left-4 right-20 bottom-[var(--pulse-mobile-nav-height)] z-30 max-w-md overflow-hidden rounded-lg border border-border bg-surface text-base-content shadow-xl md:right-auto md:bottom-4"
|
||||
aria-labelledby="github-star-title"
|
||||
aria-live="polite"
|
||||
>
|
||||
|
||||
@@ -360,3 +360,19 @@ describe('GitHubStarBanner', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GitHubStarBanner mobile navigation clearance', () => {
|
||||
it('reads the published bottom navigation height', () => {
|
||||
expect(gitHubStarBannerSource).toContain('bottom-[var(--pulse-mobile-nav-height)]');
|
||||
});
|
||||
|
||||
it('does not keep its own copy of the bar height', () => {
|
||||
// This banner was one of five sites hardcoding 5rem for a bar that
|
||||
// measures ~45px. The bar publishes its measured height instead.
|
||||
expect(gitHubStarBannerSource).not.toMatch(/5rem\s*\+\s*env\(safe-area-inset-bottom/);
|
||||
});
|
||||
|
||||
it('keeps its own desktop placement once the bar is gone', () => {
|
||||
expect(gitHubStarBannerSource).toContain('md:bottom-4');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { For, Show } from 'solid-js';
|
||||
import { For, Show, onCleanup, onMount } from 'solid-js';
|
||||
import ChevronsUpDownIcon from 'lucide-solid/icons/chevrons-up-down';
|
||||
import EllipsisIcon from 'lucide-solid/icons/ellipsis';
|
||||
import { type MobileNavBarProps, useMobileNavBarState } from './useMobileNavBarState';
|
||||
@@ -98,6 +98,45 @@ function MobileNavDestinationContent(props: {
|
||||
export function MobileNavBar(props: MobileNavBarProps) {
|
||||
const mobileNav = useMobileNavBarState(props);
|
||||
const tabIconClass = 'h-3.5 w-3.5 shrink-0';
|
||||
|
||||
// This bar's height is content-driven and includes the safe-area inset via
|
||||
// pb-safe, so anything that has to sit on top of it cannot hardcode a value.
|
||||
// Five call sites did, all at 5rem against a bar that measures ~45px, which
|
||||
// left a band below the Assistant backdrop that was neither dimmed nor
|
||||
// click-blocked. Publish the measured height instead and let them read it.
|
||||
// Reports 0 when the bar is hidden at xl, which is the correct clearance
|
||||
// there.
|
||||
let navElement: HTMLElement | undefined;
|
||||
let navObserver: ResizeObserver | undefined;
|
||||
const publishNavHeight = () => {
|
||||
if (!navElement) return;
|
||||
document.documentElement.style.setProperty(
|
||||
'--pulse-mobile-nav-height',
|
||||
`${navElement.offsetHeight}px`,
|
||||
);
|
||||
};
|
||||
// Publish from onMount, not from the ref: Solid runs refs before the node is
|
||||
// in the document, where offsetHeight is 0. Reading it after mount forces
|
||||
// layout and yields the real height, so the first paint is already correct
|
||||
// without waiting for an observer callback.
|
||||
onMount(() => {
|
||||
publishNavHeight();
|
||||
if (typeof ResizeObserver !== 'undefined' && navElement) {
|
||||
navObserver = new ResizeObserver(publishNavHeight);
|
||||
navObserver.observe(navElement);
|
||||
}
|
||||
// The observer covers content-driven height changes, but the bar is
|
||||
// xl:hidden, so crossing that breakpoint swaps between its real height and
|
||||
// 0 via display alone. Observing a display:none element is the least
|
||||
// consistent case across engines, so listen for resize too rather than
|
||||
// leaving a stale clearance behind after a rotation or window drag.
|
||||
window.addEventListener('resize', publishNavHeight);
|
||||
});
|
||||
onCleanup(() => {
|
||||
navObserver?.disconnect();
|
||||
window.removeEventListener('resize', publishNavHeight);
|
||||
document.documentElement.style.removeProperty('--pulse-mobile-nav-height');
|
||||
});
|
||||
const overflowHasBadge = () =>
|
||||
mobileNav.overflowDestinations().some(mobileNavDestinationHasBadge);
|
||||
|
||||
@@ -148,6 +187,7 @@ export function MobileNavBar(props: MobileNavBarProps) {
|
||||
|
||||
return (
|
||||
<nav
|
||||
ref={(element) => (navElement = element)}
|
||||
class="fixed inset-x-0 bottom-0 z-40 border-t border-border bg-surface pb-safe xl:hidden"
|
||||
aria-label="Mobile navigation"
|
||||
>
|
||||
|
||||
@@ -291,3 +291,48 @@ describe('MobileNavBar', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MobileNavBar clearance publishing', () => {
|
||||
const NAV_HEIGHT_VARIABLE = '--pulse-mobile-nav-height';
|
||||
|
||||
const renderBar = () =>
|
||||
render(() => (
|
||||
<MobileNavBar
|
||||
activeTab={() => 'proxmox'}
|
||||
primaryTabs={() => [makePrimary('proxmox', 'Proxmox')]}
|
||||
utilityTabs={() => [makeUtility('alerts', 'Alerts')]}
|
||||
/>
|
||||
));
|
||||
|
||||
it('publishes its measured height so overlays can clear it', () => {
|
||||
const { unmount } = renderBar();
|
||||
|
||||
// Anything sitting on top of this bar reads the published value rather
|
||||
// than hardcoding one. Five call sites once hardcoded 5rem against a bar
|
||||
// that measures ~45px, leaving a band below the Assistant backdrop that
|
||||
// was neither dimmed nor click-blocked.
|
||||
expect(document.documentElement.style.getPropertyValue(NAV_HEIGHT_VARIABLE)).toMatch(
|
||||
/^\d+(\.\d+)?px$/,
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('stops publishing once the bar is gone', () => {
|
||||
const { unmount } = renderBar();
|
||||
expect(document.documentElement.style.getPropertyValue(NAV_HEIGHT_VARIABLE)).not.toBe('');
|
||||
|
||||
unmount();
|
||||
|
||||
// A stale height would keep reserving space for a bar that is no longer
|
||||
// rendered, so consumers must fall back to the declared default.
|
||||
expect(document.documentElement.style.getPropertyValue(NAV_HEIGHT_VARIABLE)).toBe('');
|
||||
});
|
||||
|
||||
it('measures after mount rather than from the ref', () => {
|
||||
// Solid runs refs before the node is in the document, where offsetHeight
|
||||
// is 0, so publishing from the ref reports a bar of no height.
|
||||
expect(mobileNavBarSource).toContain('onMount(');
|
||||
expect(mobileNavBarSource).not.toMatch(/ref=\{setNavRef\}/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -428,8 +428,8 @@
|
||||
|
||||
@media (max-width: 79.999rem) {
|
||||
.filter-bottom-nav-aware-panel {
|
||||
bottom: calc(5rem + env(safe-area-inset-bottom)) !important;
|
||||
max-height: calc(100dvh - 6rem - env(safe-area-inset-bottom));
|
||||
bottom: var(--pulse-mobile-nav-height) !important;
|
||||
max-height: calc(100dvh - var(--pulse-mobile-nav-height) - 1rem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1159,6 +1159,21 @@ body,
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Height of the mobile bottom navigation bar. MobileNavBar overwrites this at
|
||||
runtime with its measured height, which already includes the safe-area inset
|
||||
via pb-safe - consumers must NOT add env(safe-area-inset-bottom) on top.
|
||||
|
||||
The declared value is only a pre-measurement fallback, and it deliberately
|
||||
UNDER-estimates. Reserving more than the bar's real height leaves a band
|
||||
between an overlay's backdrop and the bar that is neither dimmed nor
|
||||
click-blocked, which is the defect this variable exists to prevent; reserving
|
||||
slightly less is harmless because the opaque bar paints over it.
|
||||
*/
|
||||
:root {
|
||||
--pulse-mobile-nav-height: calc(2.5rem + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.pb-safe {
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user