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:
riyadhafraa
2026-05-19 10:59:59 +00:00
parent 9a1874fcd0
commit 7609fa95e7
2 changed files with 68 additions and 1 deletions
+52 -1
View File
@@ -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"
+16
View File
@@ -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);
}
}