From e41a730b4d90d21e6998fb321aeef84bceb29c1a Mon Sep 17 00:00:00 2001 From: isUnknown Date: Wed, 25 Mar 2026 13:20:57 +0100 Subject: [PATCH] feat: navigation swipe mobile horizontale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.svelte : swipe gauche/droite → même comportement que les touches clavier (navigation entre slides) - Play.svelte : stopImmediatePropagation sur touchend, blocage du scroll vertical (touchmove) - Portfolio.svelte : migration du touch vertical (composable) vers horizontal — navigation entre projets, slide voisine aux bords, blocage scroll vertical, debounce 650ms anti-spam Co-Authored-By: Claude Sonnet 4.6 --- src/App.svelte | 36 ++++++++++++++++++++ src/views/Play.svelte | 10 ++++++ src/views/Portfolio.svelte | 69 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/src/App.svelte b/src/App.svelte index e6ef692..ddec061 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -79,9 +79,45 @@ } } window.addEventListener('keydown', handleKeydown) + + // Swipe gauche/droite → même comportement que les touches clavier + const TOUCH_THRESHOLD = 50 + let touchStartX = 0 + let touchStartY = 0 + + const handleTouchStart = (e) => { + touchStartX = e.touches[0].clientX + touchStartY = e.touches[0].clientY + } + + const handleTouchEnd = (e) => { + const deltaX = touchStartX - e.changedTouches[0].clientX + const deltaY = touchStartY - e.changedTouches[0].clientY + if (Math.abs(deltaX) < TOUCH_THRESHOLD) return + if (Math.abs(deltaY) > Math.abs(deltaX)) return + + const activePath = slides.active?.path ?? '' + const currentPath = window.location.pathname.replace(/^\/en/, '') || '/' + const isSubPage = activePath && currentPath.startsWith(activePath + '/') + if (isSubPage) return + + if (deltaX > 0) { + const next = slides.all[slides.activeIndex + 1] + if (next) slideTo(next.path) + } else { + const prev = slides.all[slides.activeIndex - 1] + if (prev) slideTo(prev.path) + } + } + + window.addEventListener('touchstart', handleTouchStart, { passive: true }) + window.addEventListener('touchend', handleTouchEnd, { passive: true }) + return () => { window.removeEventListener('resize', handleResize) window.removeEventListener('keydown', handleKeydown) + window.removeEventListener('touchstart', handleTouchStart) + window.removeEventListener('touchend', handleTouchEnd) clearTimeout(resizeTimer) } }) diff --git a/src/views/Play.svelte b/src/views/Play.svelte index 0dbb338..9e61135 100644 --- a/src/views/Play.svelte +++ b/src/views/Play.svelte @@ -101,6 +101,8 @@ function onTouchEnd(e) { if (!isActive) return + // Toujours intercepter : Play gère lui-même toute la navigation touch + e.stopImmediatePropagation() const deltaX = touchStartX - e.changedTouches[0].clientX if (Math.abs(deltaX) < TOUCH_THRESHOLD) return @@ -137,14 +139,22 @@ } }) + // Bloque le scroll vertical natif (pull-to-refresh, bounce) quand la slide est active + function onTouchMove(e) { + if (!isActive) return + e.preventDefault() + } + onMount(() => { window.addEventListener('keydown', onKeyDown, { capture: true }) window.addEventListener('touchstart', onTouchStart, { capture: true, passive: true }) window.addEventListener('touchend', onTouchEnd, { capture: true, passive: true }) + window.addEventListener('touchmove', onTouchMove, { passive: false }) return () => { window.removeEventListener('keydown', onKeyDown, { capture: true }) window.removeEventListener('touchstart', onTouchStart, { capture: true }) window.removeEventListener('touchend', onTouchEnd, { capture: true }) + window.removeEventListener('touchmove', onTouchMove) clearTimeout(t1) clearTimeout(t2) clearTimeout(t3) diff --git a/src/views/Portfolio.svelte b/src/views/Portfolio.svelte index 7d2d58f..f1cc928 100644 --- a/src/views/Portfolio.svelte +++ b/src/views/Portfolio.svelte @@ -1,6 +1,7 @@