diff --git a/artifacts/tx-os/src/components/app-dock.tsx b/artifacts/tx-os/src/components/app-dock.tsx index 02296671..bf917322 100644 --- a/artifacts/tx-os/src/components/app-dock.tsx +++ b/artifacts/tx-os/src/components/app-dock.tsx @@ -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(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 (