diff --git a/src/components/SasquatchGuide.tsx b/src/components/SasquatchGuide.tsx index 88ac9e3..2e8dbc1 100644 --- a/src/components/SasquatchGuide.tsx +++ b/src/components/SasquatchGuide.tsx @@ -392,14 +392,15 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) { setDragPos({ x: newX, y: newY }); }, [setDragPos]); + // Single source of truth for opening the panel. Sets the "just opened" lock + // so a stray follow-up event from the same tap can't immediately close it. + const openPanel = useCallback(() => { + justOpenedRef.current = Date.now(); + setIsMinimized(false); + setIsVisible(true); + }, [setIsMinimized]); + const onPointerUp = useCallback((e: React.PointerEvent) => { - // Suppress the synthesized mouse "click" that follows a touch pointerup. - // Without this, on mobile WebViews the ghost click lands on whatever element - // is now under the finger (the panel unmounts the bubble the instant we - // open) and can immediately re-close the panel โ€” the "opens then closes" - // bug. - e.preventDefault(); - e.stopPropagation(); try { if (dragRef.current) dragRef.current.releasePointerCapture(e.pointerId); } catch { @@ -409,15 +410,24 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) { dragStart.current = null; dragMoved.current = false; setTimeout(() => setIsDragging(false), 50); + // If this pointer sequence was a genuine tap (no real drag), open here. + // A parallel onClick handler also opens as a reliable fallback for mobile + // WebViews where the pointer-capture tap can be flaky on first interaction; + // openPanel is idempotent so double-firing is harmless. if (!wasDrag) { - // Always open the panel โ€” even on pages without a context message. - // Set a short "just opened" lock so a stray follow-up event from the same - // tap can't immediately minimize/close it. - justOpenedRef.current = Date.now(); - setIsMinimized(false); - setIsVisible(true); + openPanel(); } - }, [setIsMinimized]); + }, [openPanel]); + + // Reliable open path: plain click. On mobile WebViews the pointer-capture tap + // detection can miss the very first interaction after mount (the "1st tap does + // nothing, 2nd works" bug). A normal onClick is the most dependable "I tapped + // this" signal, so we use it as a fallback โ€” but ignore it if the gesture was + // actually a drag. + const onBubbleClick = useCallback(() => { + if (dragMoved.current) return; + openPanel(); + }, [openPanel]); const bubbleStyle = useMemo(() => { if (dragPos) { @@ -682,6 +692,7 @@ Instructions: onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} + onClick={onBubbleClick} title="Press and drag to move ยท Tap to open" >