From b517819c84759750a3a96d21862495ad3e515b95 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 7 Jun 2026 21:05:58 -0500 Subject: [PATCH] feat(desktop): hover-intent + refined easing for sidebar reveal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gate the reveal on pointer velocity: the full-height edge hot-zone now only arms on a slow, deliberate pass (<=0.55 px/ms). Fast sweeps toward the titlebar/statusbar — or off the window — blow past the threshold and never trigger, so the wide hit area stops being a nuisance. - Swap the slide easing to cubic-bezier(0.32,0.72,0,1) at 260ms (snappy-out, soft-land) for a more serious-app feel. --- .../src/components/pane-shell/pane-shell.tsx | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/components/pane-shell/pane-shell.tsx b/apps/desktop/src/components/pane-shell/pane-shell.tsx index 53f56f6350d..4b46797c6fb 100644 --- a/apps/desktop/src/components/pane-shell/pane-shell.tsx +++ b/apps/desktop/src/components/pane-shell/pane-shell.tsx @@ -70,6 +70,11 @@ interface CollectedPane { const DEFAULT_WIDTH = '16rem' const DEFAULT_RESIZE_MIN_WIDTH = 160 +// Hover-intent gate: only arm the reveal when the pointer is moving slowly +// inside the edge zone. A fast sweep (heading for the titlebar/statusbar or +// off the window) blows past this threshold and never triggers. +const HOVER_INTENT_MAX_SPEED = 0.55 // px per ms + const widthToCss = (value: WidthValue | undefined, fallback: string) => value === undefined ? fallback : typeof value === 'number' ? `${value}px` : value @@ -214,8 +219,33 @@ export function Pane({ const paneStates = useStore($paneStates) const registered = useRef(false) const paneRef = useRef(null) + const lastSample = useRef<{ t: number; x: number; y: number } | null>(null) const [hoverRevealed, setHoverRevealed] = useState(false) + // Arm the reveal only on a slow, deliberate pass through the edge zone — + // ignore fast fly-bys (toward the titlebar/statusbar, or leaving the window). + const onEdgeMove = useCallback((e: ReactPointerEvent) => { + const prev = lastSample.current + const now = e.timeStamp + lastSample.current = { t: now, x: e.clientX, y: e.clientY } + + if (!prev) { + return + } + + const dt = now - prev.t + + if (dt <= 0) { + return + } + + const speed = Math.hypot(e.clientX - prev.x, e.clientY - prev.y) / dt + + if (speed <= HOVER_INTENT_MAX_SPEED) { + setHoverRevealed(true) + } + }, []) + useEffect(() => { if (registered.current) { return @@ -322,7 +352,7 @@ export function Pane({ ref={paneRef} style={{ gridColumn: `${slot.column} / ${slot.column + 1}` }} > - {/* Invisible edge hot-zone — hovering/focusing it floats the panel in. */} + {/* Invisible edge hot-zone — a slow, intentful pass floats the panel in. */}