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:
@@ -1,3 +1,4 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
import { useDock, MusicPlayerHost } from 'officerdev';
|
import { useDock, MusicPlayerHost } from 'officerdev';
|
||||||
import { Background } from './Background';
|
import { Background } from './Background';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
@@ -12,20 +13,19 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|||||||
const { items: visibleItems } = useDock(ALL_DOCK_ITEMS, DEFAULT_DOCK_PATHS);
|
const { items: visibleItems } = useDock(ALL_DOCK_ITEMS, DEFAULT_DOCK_PATHS);
|
||||||
const isTouch = useIsTouch();
|
const isTouch = useIsTouch();
|
||||||
usePageTitleSync();
|
usePageTitleSync();
|
||||||
|
// The content region shrinks when the (in-flow) music dock takes its space; the nav dock measures its
|
||||||
|
// reveal boundary from this element, so it always sits just above whatever's at the bottom.
|
||||||
|
const regionRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative overflow-hidden h-dvh outline-none inset-0">
|
<div className="relative flex h-dvh flex-col overflow-hidden outline-none inset-0">
|
||||||
<Header dockItems={visibleItems} />
|
<Header dockItems={visibleItems} />
|
||||||
<section className="relative h-dvh snap-start overflow-hidden">
|
<section ref={regionRef} className="relative min-h-0 flex-1 snap-start overflow-hidden">
|
||||||
<Background />
|
<Background />
|
||||||
{!isTouch && <Dock items={visibleItems} className="hidden md:flex" />}
|
{!isTouch && <Dock items={visibleItems} boundaryRef={regionRef} className="hidden md:flex" />}
|
||||||
<div className="absolute inset-0 z-2 pt-[52px] md:pt-[64px] pb-2 overflow-y-auto">
|
<div className="absolute inset-0 z-2 pt-[52px] md:pt-[64px] pb-2 overflow-y-auto">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
<MusicPlayerHost />
|
<MusicPlayerHost />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Link, useLocation } from 'react-router';
|
import { Link, useLocation } from 'react-router';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
import { useMusicPlayer, MUSIC_DOCK_HEIGHT } from 'officerdev';
|
|
||||||
|
|
||||||
export type DockItem = {
|
export type DockItem = {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -13,6 +12,9 @@ export type DockItem = {
|
|||||||
type DockProps = {
|
type DockProps = {
|
||||||
items: DockItem[];
|
items: DockItem[];
|
||||||
className?: string;
|
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;
|
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));
|
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 [mouseX, setMouseX] = useState<number | null>(null);
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const dockRef = useRef<HTMLDivElement | null>(null);
|
const dockRef = useRef<HTMLDivElement | null>(null);
|
||||||
const location = useLocation();
|
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));
|
const isActive = (to: string) => (to === '/' ? location.pathname === '/' : location.pathname.startsWith(to));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMouseMove = (ev: MouseEvent) => {
|
const handleMouseMove = (ev: MouseEvent) => {
|
||||||
const distFromBottom = window.innerHeight - ev.clientY;
|
// Distance from the bottom of the content region (which the in-flow music dock shrinks when
|
||||||
// When the music dock is present the nav dock rides MUSIC_DOCK_HEIGHT higher, so lift the hide/
|
// present) — so reveal/hide triggers just above wherever the region ends, above the music dock.
|
||||||
// magnify zone by the same amount — otherwise hovering the raised dock counts as past the hide
|
const bottom = boundaryRef.current?.getBoundingClientRect().bottom ?? window.innerHeight;
|
||||||
// threshold and it slides away under the cursor. The reveal trigger stays at the bottom edge.
|
const distFromBottom = bottom - ev.clientY;
|
||||||
const lift = musicPresentRef.current ? MUSIC_DOCK_HEIGHT : 0;
|
setVisible((prev) => (prev ? distFromBottom <= HIDE_THRESHOLD : distFromBottom <= SHOW_THRESHOLD));
|
||||||
const hideAt = HIDE_THRESHOLD + lift;
|
if (distFromBottom <= HIDE_THRESHOLD) {
|
||||||
setVisible((prev) => (prev ? distFromBottom <= hideAt : distFromBottom <= SHOW_THRESHOLD));
|
|
||||||
if (distFromBottom <= hideAt) {
|
|
||||||
const rect = dockRef.current?.getBoundingClientRect();
|
const rect = dockRef.current?.getBoundingClientRect();
|
||||||
if (rect) setMouseX(ev.clientX - rect.left);
|
if (rect) setMouseX(ev.clientX - rect.left);
|
||||||
} else {
|
} else {
|
||||||
@@ -63,20 +57,17 @@ export const Dock = ({ items, className }: DockProps) => {
|
|||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
return () => document.removeEventListener('mousemove', handleMouseMove);
|
return () => document.removeEventListener('mousemove', handleMouseMove);
|
||||||
}, []);
|
}, [boundaryRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={dockRef}
|
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={{
|
style={{
|
||||||
backgroundColor: 'var(--dock-bg)',
|
backgroundColor: 'var(--dock-bg)',
|
||||||
borderColor: 'var(--dock-border)',
|
borderColor: 'var(--dock-border)',
|
||||||
bottom: '16px',
|
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 ? '0' : 'calc(100% + 24px)'})`,
|
||||||
transform: `translateX(-50%) translateY(${
|
|
||||||
visible ? (musicDockPresent ? `-${MUSIC_DOCK_HEIGHT}px` : '0') : 'calc(100% + 24px)'
|
|
||||||
})`,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{items.map((item, index) => {
|
{items.map((item, index) => {
|
||||||
|
|||||||
@@ -249,8 +249,10 @@ export const MusicPlayerHost = () => {
|
|||||||
|
|
||||||
if (!current) return null;
|
if (!current) return null;
|
||||||
|
|
||||||
|
// In-flow bottom bar (NOT position:fixed) — it reserves its own height so the content above shrinks to
|
||||||
|
// fit and the nav dock naturally sits above it, no overlap hacks needed.
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-x-0 bottom-0 z-50 flex items-center gap-3 border-t border-border bg-card/95 px-4 py-2 backdrop-blur">
|
<div className="flex shrink-0 items-center gap-3 border-t border-border bg-card/95 px-4 py-2 backdrop-blur">
|
||||||
{/* cover + info — click to open this album in /music */}
|
{/* cover + info — click to open this album in /music */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ export type MusicPlayerState = {
|
|||||||
|
|
||||||
const INITIAL: MusicPlayerState = { queue: [], index: 0, playing: false };
|
const INITIAL: MusicPlayerState = { queue: [], index: 0, playing: false };
|
||||||
|
|
||||||
// Amount (px) the nav Dock shifts up while the music dock is present, so it clears it.
|
|
||||||
export const MUSIC_DOCK_HEIGHT = 72;
|
|
||||||
|
|
||||||
export function useMusicPlayer() {
|
export function useMusicPlayer() {
|
||||||
const [state, setState] = useGlobal<MusicPlayerState>('MUSIC_PLAYER', INITIAL);
|
const [state, setState] = useGlobal<MusicPlayerState>('MUSIC_PLAYER', INITIAL);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user