web: music dock joins the layout flow — removes the nav-dock overlap hacks

The music dock was position:fixed, overlaying the bottom of the content, so the
nav dock needed a pile of hacks to dodge it: a hand-measured MUSIC_DOCK_HEIGHT
(72) constant, a presence flag, a translateY lift, and a matching hover-threshold
lift (via a ref) so it wouldn't slide away under the cursor. Fragile the moment
the music dock's height changed.

Now the dock is an in-flow bottom bar that reserves its own height:
- DashboardLayout is a flex column: the content region (flex-1, min-h-0) shrinks
  when the music dock takes its space; MusicPlayerHost renders an in-flow bar
  (shrink-0) instead of a fixed overlay.
- The nav Dock is absolute within the content region and measures its reveal/hide
  boundary from that region's bottom edge (a boundaryRef) — so it always sits just
  above whatever's at the bottom, music dock or not, with zero knowledge of it.
- Deleted MUSIC_DOCK_HEIGHT, musicDockPresent, the lift, and the transform hack.

Bonus: content at the very bottom is no longer hidden under the fixed dock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 15:05:36 +00:00
co-authored by Claude Opus 4.8
parent cc141ae91a
commit 0a8ec845ef
4 changed files with 25 additions and 35 deletions
@@ -1,7 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { Link, useLocation } from 'react-router';
import type { LucideIcon } from 'lucide-react';
import { useMusicPlayer, MUSIC_DOCK_HEIGHT } from 'officerdev';
export type DockItem = {
label: string;
@@ -13,6 +12,9 @@ export type DockItem = {
type DockProps = {
items: DockItem[];
className?: string;
// The content region the dock lives in. Reveal/hide is measured from ITS bottom edge, which the
// in-flow music dock shrinks when present — so the dock clears the music dock with no magic offset.
boundaryRef: React.RefObject<HTMLElement | null>;
};
const ICON_SIZE = 48;
@@ -30,30 +32,22 @@ const getScale = (mouseX: number | null, iconCenterX: number) => {
return 1 + (MAX_SCALE - 1) * Math.cos((distance / MAX_DISTANCE) * (Math.PI / 2));
};
export const Dock = ({ items, className }: DockProps) => {
export const Dock = ({ items, className, boundaryRef }: DockProps) => {
const [mouseX, setMouseX] = useState<number | null>(null);
const [visible, setVisible] = useState(false);
const dockRef = useRef<HTMLDivElement | null>(null);
const location = useLocation();
const { current } = useMusicPlayer();
const musicDockPresent = !!current;
// Read the latest value inside the (once-registered) mousemove handler, so it reacts to the music
// dock appearing/disappearing without re-binding the listener.
const musicPresentRef = useRef(musicDockPresent);
musicPresentRef.current = musicDockPresent;
const isActive = (to: string) => (to === '/' ? location.pathname === '/' : location.pathname.startsWith(to));
useEffect(() => {
const handleMouseMove = (ev: MouseEvent) => {
const distFromBottom = window.innerHeight - ev.clientY;
// When the music dock is present the nav dock rides MUSIC_DOCK_HEIGHT higher, so lift the hide/
// magnify zone by the same amount — otherwise hovering the raised dock counts as past the hide
// threshold and it slides away under the cursor. The reveal trigger stays at the bottom edge.
const lift = musicPresentRef.current ? MUSIC_DOCK_HEIGHT : 0;
const hideAt = HIDE_THRESHOLD + lift;
setVisible((prev) => (prev ? distFromBottom <= hideAt : distFromBottom <= SHOW_THRESHOLD));
if (distFromBottom <= hideAt) {
// Distance from the bottom of the content region (which the in-flow music dock shrinks when
// present) — so reveal/hide triggers just above wherever the region ends, above the music dock.
const bottom = boundaryRef.current?.getBoundingClientRect().bottom ?? window.innerHeight;
const distFromBottom = bottom - ev.clientY;
setVisible((prev) => (prev ? distFromBottom <= HIDE_THRESHOLD : distFromBottom <= SHOW_THRESHOLD));
if (distFromBottom <= HIDE_THRESHOLD) {
const rect = dockRef.current?.getBoundingClientRect();
if (rect) setMouseX(ev.clientX - rect.left);
} else {
@@ -63,20 +57,17 @@ export const Dock = ({ items, className }: DockProps) => {
document.addEventListener('mousemove', handleMouseMove);
return () => document.removeEventListener('mousemove', handleMouseMove);
}, []);
}, [boundaryRef]);
return (
<div
ref={dockRef}
className={`fixed left-1/2 z-5 items-end gap-2 md:gap-6 px-2 py-1.5 md:px-3 md:py-2 rounded-2xl border backdrop-blur-xl shadow-lg transition-transform duration-300 ease-in-out ${className ?? 'flex'}`}
className={`absolute left-1/2 z-5 items-end gap-2 md:gap-6 px-2 py-1.5 md:px-3 md:py-2 rounded-2xl border backdrop-blur-xl shadow-lg transition-transform duration-300 ease-in-out ${className ?? 'flex'}`}
style={{
backgroundColor: 'var(--dock-bg)',
borderColor: 'var(--dock-border)',
bottom: '16px',
// Slide up over the site-wide music dock when it's present so the nav dock clears it.
transform: `translateX(-50%) translateY(${
visible ? (musicDockPresent ? `-${MUSIC_DOCK_HEIGHT}px` : '0') : 'calc(100% + 24px)'
})`,
transform: `translateX(-50%) translateY(${visible ? '0' : 'calc(100% + 24px)'})`,
}}
>
{items.map((item, index) => {