diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index 7673ea1..c7d9cef 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -429,84 +429,18 @@ export const TerminalComponent = React.forwardRef( }; document.addEventListener("visibilitychange", handleVisibilityChange); - // Mobile touch scrolling — translate vertical swipe to terminal scroll. - // We attach to the container (wrapper) in CAPTURE phase so we run before - // xterm.js internals stop propagation. We also listen for wheel events - // so that mobile browsers that translate touch-pan into synthetic wheel - // events will still scroll the buffer. - let touchStartY = 0; - let touchStartX = 0; - let isTouchScrolling = false; - 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; - } - }; - - const handleTouchMove = (e: TouchEvent) => { - if (e.touches.length !== 1 || !termRef.current) return; - const touch = e.touches[0]; - 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) > 6) { - if (!isTouchScrolling) isTouchScrolling = true; - e.preventDefault(); - e.stopPropagation(); - 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; - accumulatedDeltaY = 0; - }; - - const handleWheel = (e: WheelEvent) => { - if (!termRef.current) return; - // On mobile some browsers translate vertical pan into wheel events. - // xterm.js already handles wheel natively on desktop, but on mobile - // the synthetic wheel may not reach xterm because of our layout. - // We manually forward vertical wheel deltas to scrollLines. - if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) { - e.preventDefault(); - const lines = Math.round(e.deltaY / 16); - if (lines !== 0) { - termRef.current.scrollLines(lines); - } - } - }; - + // Enable native touch scrolling on xterm.js viewport for mobile. + // xterm.js creates an internal .xterm-viewport div that has overflow-y + // scroll but may disable touch-action. We override it so the browser + // handles vertical touch panning natively. if (isMobile) { - // Capture phase runs before xterm.js handlers on the child elements - container.addEventListener("touchstart", handleTouchStart, { - passive: true, - capture: true, - }); - container.addEventListener("touchmove", handleTouchMove, { - passive: false, - capture: true, - }); - container.addEventListener("touchend", handleTouchEnd, { - capture: true, - }); - container.addEventListener("wheel", handleWheel, { - passive: false, - capture: true, - }); + const viewport = container.querySelector( + ".xterm-viewport", + ) as HTMLElement | null; + if (viewport) { + viewport.style.touchAction = "pan-y"; + viewport.style.overscrollBehavior = "contain"; + } } return () => { @@ -520,20 +454,6 @@ export const TerminalComponent = React.forwardRef( "visibilitychange", handleVisibilityChange, ); - if (isMobile) { - container.removeEventListener("touchstart", handleTouchStart, { - capture: true, - }); - container.removeEventListener("touchmove", handleTouchMove, { - capture: true, - }); - container.removeEventListener("touchend", handleTouchEnd, { - capture: true, - }); - container.removeEventListener("wheel", handleWheel, { - capture: true, - }); - } if (ws) { ws.close(1000, "Component unmounting"); } diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index edd203a..92c24be 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -3709,6 +3709,14 @@ a.nav-item, /* xterm.js manages its own scrolling and viewport dimensions */ +/* Mobile: enable native touch scrolling on xterm viewport */ +.terminal-wrapper.mobile .xterm .xterm-viewport { + touch-action: pan-y !important; + -webkit-overflow-scrolling: touch !important; + overflow-y: auto !important; + scrollbar-width: none; +} + /* Special Keys Strip */ .special-keys-strip { flex-shrink: 0;