fix(desktop): measure dock proximity against the chat surface, not the window

The dock target is the docked composer at the bottom-center of its own
surface. In a split, the viewport's bottom-center is somewhere else, so
dragging onto the real dock never registered.
This commit is contained in:
Brooklyn Nicholson 2026-07-28 02:50:06 -05:00
parent df25d77131
commit 88b6a7a49c

View file

@ -3,6 +3,7 @@ import { type PointerEvent as ReactPointerEvent, type RefObject, useCallback, us
import {
POPOUT_ESTIMATED_HEIGHT,
POPOUT_WIDTH_REM,
type PopoutBounds,
type PopoutPosition,
type PopoutSize,
readPopoutBounds,
@ -35,6 +36,8 @@ interface PressState {
interface ComposerPopoutGesturesOptions {
composerRef: RefObject<HTMLFormElement | null>
/** Layout zone this composer belongs to — the scope its float is stored under. */
groupId: string
onDock: () => void
onPopOut: () => void
poppedOut: boolean
@ -67,10 +70,17 @@ function isFloatDragPlatform(target: EventTarget | null) {
}
/** 0 (far) → 1 (inside the dock zone). Drives both the dock glow and the
* release-to-dock test (which fires at proximity 1). */
function dockProximityOf(rect: DOMRect) {
const horizontalDist = Math.abs(rect.left + rect.width / 2 - window.innerWidth / 2)
const verticalGap = window.innerHeight - DOCK_ZONE_BOTTOM_PX - rect.bottom
* release-to-dock test (which fires at proximity 1).
*
* Measured against THIS surface's area, not the window: the dock target is the
* docked composer, which sits at the bottom-center of its own chat surface. In
* a split (or any layout where the chat isn't the full window) the viewport's
* bottom-center is somewhere else entirely, so dragging onto the real dock
* never registered. */
function dockProximityOf(rect: DOMRect, area?: PopoutBounds) {
const a = area ?? { bottom: window.innerHeight, left: 0, right: window.innerWidth, top: 0 }
const horizontalDist = Math.abs(rect.left + rect.width / 2 - (a.left + a.right) / 2)
const verticalGap = a.bottom - DOCK_ZONE_BOTTOM_PX - rect.bottom
const v = verticalGap <= 0 ? 1 : Math.max(0, 1 - verticalGap / DOCK_VERTICAL_FALLOFF_PX)
@ -109,6 +119,7 @@ function popoutPositionUnderPointer(
*/
export function useComposerPopoutGestures({
composerRef,
groupId,
onDock,
onPopOut,
poppedOut,
@ -142,7 +153,12 @@ export function useComposerPopoutGestures({
const beginFloatDrag = useCallback(
(state: PressState, clientX: number, clientY: number, next: PopoutPosition, size?: PopoutSize) => {
clearTimer()
const clamped = setComposerPopoutPosition(next, { area: readPopoutBounds(composerRef.current), size })
const clamped = setComposerPopoutPosition(groupId, next, {
area: readPopoutBounds(composerRef.current),
size
})
liveRef.current = clamped
state.mode = 'float'
@ -154,7 +170,7 @@ export function useComposerPopoutGestures({
setDragging(true)
},
[clearTimer, composerRef]
[clearTimer, composerRef, groupId]
)
const peelOffFromDock = useCallback(
@ -255,17 +271,19 @@ export function useComposerPopoutGestures({
const composer = composerRef.current
const size = composer ? { height: composer.offsetHeight, width: composer.offsetWidth } : undefined
const area = readPopoutBounds(composer)
liveRef.current = setComposerPopoutPosition(
groupId,
{
bottom: state.startBottom - (pending.y - state.startY),
right: state.startRight - (pending.x - state.startX)
},
{ area: readPopoutBounds(composer), size }
{ area, size }
)
if (composer) {
setDockProximity(dockProximityOf(composer.getBoundingClientRect()))
setDockProximity(dockProximityOf(composer.getBoundingClientRect(), area))
}
}
@ -317,13 +335,14 @@ export function useComposerPopoutGestures({
if (state.armed && state.mode === 'float') {
const composer = composerRef.current
const rect = composer?.getBoundingClientRect()
const area = readPopoutBounds(composer)
if (rect && dockProximityOf(rect) >= 1) {
if (rect && dockProximityOf(rect, area) >= 1) {
onDock()
} else {
// Persist the resting position once, on release — never per move.
const size = composer ? { height: composer.offsetHeight, width: composer.offsetWidth } : undefined
setComposerPopoutPosition(liveRef.current, { area: readPopoutBounds(composer), persist: true, size })
setComposerPopoutPosition(groupId, liveRef.current, { area, persist: true, size })
}
}
@ -340,7 +359,7 @@ export function useComposerPopoutGestures({
window.removeEventListener('pointerup', handleUp)
window.removeEventListener('pointercancel', handleUp)
}
}, [composerRef, onDock, peelOffFromDock, resetGesture])
}, [composerRef, groupId, onDock, peelOffFromDock, resetGesture])
useEffect(() => clearTimer, [clearTimer])