Fix AppDock covering last row on mobile (#608)
Problem: The floating AppDock (position: fixed at bottom-2/3) overlapped the last row of long pages on mobile — e.g. meeting #7 on the Meetings page was hidden behind the dock with no way to scroll it into view. Fix: - AppDock measures its rendered outer-bottom extent (height + bottom offset + safe-area inset) via getBoundingClientRect() and publishes it as `--app-dock-height` on documentElement, with a small 8px gap so the last row doesn't kiss the dock. - Re-measures on ResizeObserver, window resize, and orientationchange so the value stays correct across rotation, URL-bar collapse, and dock content changes. - Clears the variable on unmount and whenever the dock hides (≤1 other app), so pages without a dock get no extra padding. - Global rule in index.css: `body { padding-bottom: var(--app-dock-height, 0px) }`, scoped to `@media not print` so PDF exports stay unaffected. Files: - artifacts/tx-os/src/components/app-dock.tsx - artifacts/tx-os/src/index.css No deviations from the task plan.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as LucideIcons from "lucide-react";
|
||||
@@ -32,6 +33,7 @@ export function AppDock({ currentSlug }: AppDockProps) {
|
||||
const { i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
const { data: apps } = useListApps({ query: { queryKey: getListAppsQueryKey() } });
|
||||
const dockRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// Match Home's source-of-truth contract: same `useListApps` query
|
||||
// key and the same `isActive` filter Home applies in its
|
||||
@@ -45,7 +47,55 @@ export function AppDock({ currentSlug }: AppDockProps) {
|
||||
|
||||
// Skip the degenerate single-button dock: if the user only has one
|
||||
// other app to switch to, the dock adds no real value.
|
||||
if (others.length <= 1) return null;
|
||||
const visible = others.length > 1;
|
||||
|
||||
// Publish the dock's measured outer height as `--app-dock-height` on
|
||||
// the document root so the global `body { padding-bottom }` rule in
|
||||
// index.css can reserve space below page content. Without this, the
|
||||
// floating dock (position: fixed) overlaps the last row on long
|
||||
// mobile pages (e.g. Meetings, Notes, My Orders).
|
||||
//
|
||||
// We re-measure on resize and on `others.length` changes so the
|
||||
// padding stays in sync as the dock grows/shrinks or the safe-area
|
||||
// inset changes (iOS rotation, URL-bar collapse).
|
||||
useLayoutEffect(() => {
|
||||
if (!visible) return;
|
||||
const root = document.documentElement;
|
||||
const el = dockRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const update = () => {
|
||||
// Use getBoundingClientRect so we capture the actual rendered
|
||||
// height including the `bottom-2/3` offset (we want the total
|
||||
// space the dock occupies from the bottom of the viewport).
|
||||
const rect = el.getBoundingClientRect();
|
||||
const viewportH = window.innerHeight;
|
||||
const occupied = Math.max(0, viewportH - rect.top);
|
||||
// A small breathing gap so the last row doesn't kiss the dock.
|
||||
root.style.setProperty("--app-dock-height", `${Math.ceil(occupied) + 8}px`);
|
||||
};
|
||||
|
||||
update();
|
||||
const ro = new ResizeObserver(update);
|
||||
ro.observe(el);
|
||||
window.addEventListener("resize", update);
|
||||
window.addEventListener("orientationchange", update);
|
||||
return () => {
|
||||
ro.disconnect();
|
||||
window.removeEventListener("resize", update);
|
||||
window.removeEventListener("orientationchange", update);
|
||||
root.style.removeProperty("--app-dock-height");
|
||||
};
|
||||
}, [visible, others.length]);
|
||||
|
||||
// When the dock is hidden, ensure we clear any stale variable so the
|
||||
// page doesn't keep extra padding from a prior render.
|
||||
useEffect(() => {
|
||||
if (visible) return;
|
||||
document.documentElement.style.removeProperty("--app-dock-height");
|
||||
}, [visible]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
const openApp = (app: App) => {
|
||||
logAppOpen(app.id).catch(() => {});
|
||||
@@ -63,6 +113,7 @@ export function AppDock({ currentSlug }: AppDockProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dockRef}
|
||||
className="fixed inset-x-0 bottom-2 sm:bottom-3 z-40 flex justify-center print:hidden pointer-events-none"
|
||||
style={{ paddingBottom: "env(safe-area-inset-bottom)" }}
|
||||
data-testid="app-dock"
|
||||
|
||||
@@ -453,4 +453,20 @@ html[dir="rtl"] .truncate {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/*
|
||||
* #608: The floating AppDock (position: fixed at bottom) overlaps the
|
||||
* last row of long pages on mobile (Meetings, Notes, My Orders, etc.).
|
||||
* The dock measures itself at mount and publishes its total occupied
|
||||
* height as `--app-dock-height` on :root; we reserve matching space
|
||||
* below the body so the user can always scroll the final row above
|
||||
* the dock. When the dock is hidden (≤1 other app), the variable is
|
||||
* cleared and the fallback `0px` keeps pages flush as before. Skipped
|
||||
* for print so the dock space doesn't leak into PDF exports.
|
||||
*/
|
||||
@media not print {
|
||||
body {
|
||||
padding-bottom: var(--app-dock-height, 0px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user