diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index 76f9d68..0fea4c0 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -430,16 +430,20 @@ export const TerminalComponent = React.forwardRef( document.addEventListener("visibilitychange", handleVisibilityChange); // Mobile touch scrolling — translate vertical swipe to terminal scroll + // Attach to term.element (xterm root) rather than container wrapper, + // since xterm internals may intercept events before they bubble up. + const touchTarget = term.element || container; let touchStartY = 0; let touchStartX = 0; let isTouchScrolling = false; - let touchScrollRaf: number | null = null; + let accumulatedDeltaY = 0; const handleTouchStart = (e: TouchEvent) => { if (e.touches.length === 1) { touchStartY = e.touches[0].clientY; touchStartX = e.touches[0].clientX; isTouchScrolling = false; + accumulatedDeltaY = 0; } }; @@ -449,34 +453,34 @@ export const TerminalComponent = React.forwardRef( const deltaY = touchStartY - touch.clientY; const deltaX = Math.abs(touchStartX - touch.clientX); // If vertical movement dominates and exceeds threshold, scroll terminal buffer - if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 10) { + if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 6) { if (!isTouchScrolling) isTouchScrolling = true; e.preventDefault(); - if (touchScrollRaf) cancelAnimationFrame(touchScrollRaf); - touchScrollRaf = requestAnimationFrame(() => { - if (!termRef.current) return; - const lines = Math.round(deltaY / 30); - if (lines !== 0) { - termRef.current.scrollLines(lines); - touchStartY = touch.clientY; - } - touchScrollRaf = null; - }); + accumulatedDeltaY += deltaY; + touchStartY = touch.clientY; + const pxPerLine = 16; + const lines = Math.round(accumulatedDeltaY / pxPerLine); + if (lines !== 0) { + // Negative scrollLines = scroll up (show older buffer content) + termRef.current.scrollLines(-lines); + accumulatedDeltaY = 0; + } } }; const handleTouchEnd = () => { isTouchScrolling = false; - if (touchScrollRaf) { - cancelAnimationFrame(touchScrollRaf); - touchScrollRaf = null; - } + accumulatedDeltaY = 0; }; if (isMobile) { - container.addEventListener("touchstart", handleTouchStart, { passive: true }); - container.addEventListener("touchmove", handleTouchMove, { passive: false }); - container.addEventListener("touchend", handleTouchEnd); + touchTarget.addEventListener("touchstart", handleTouchStart, { + passive: true, + }); + touchTarget.addEventListener("touchmove", handleTouchMove, { + passive: false, + }); + touchTarget.addEventListener("touchend", handleTouchEnd); } return () => { @@ -491,11 +495,10 @@ export const TerminalComponent = React.forwardRef( handleVisibilityChange, ); if (isMobile) { - container.removeEventListener("touchstart", handleTouchStart); - container.removeEventListener("touchmove", handleTouchMove); - container.removeEventListener("touchend", handleTouchEnd); + touchTarget.removeEventListener("touchstart", handleTouchStart); + touchTarget.removeEventListener("touchmove", handleTouchMove); + touchTarget.removeEventListener("touchend", handleTouchEnd); } - if (touchScrollRaf) cancelAnimationFrame(touchScrollRaf); if (ws) { ws.close(1000, "Component unmounting"); } @@ -624,7 +627,9 @@ export const TerminalComponent = React.forwardRef( }; return ( -
+
{showControls && (
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index edd203a..85448f6 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -3703,6 +3703,7 @@ a.nav-item, padding: 0; overflow: hidden; position: relative; + touch-action: none; } /* xterm.js manages its own sizing */