diff --git a/src/components/Cell.vue b/src/components/Cell.vue index 5f39c8b..13ea95b 100644 --- a/src/components/Cell.vue +++ b/src/components/Cell.vue @@ -34,35 +34,30 @@ const clearLongPress = () => { const handlePointerDown = (e) => { if (e.pointerType === 'mouse') { - if (e.button === 0) emit('start-drag', props.r, props.c, false); - if (e.button === 2) emit('start-drag', props.r, props.c, true); + if (e.button === 0) emit('start-drag', props.r, props.c, false, false); + if (e.button === 2) emit('start-drag', props.r, props.c, true, false); return; } - longPressTriggered = false; - clearLongPress(); - longPressTimer = setTimeout(() => { - longPressTriggered = true; - emit('start-drag', props.r, props.c, true); - }, 450); + + // Touch logic + const now = Date.now(); + if (now - lastTap < 300) { + // Double tap -> X (Force) + emit('start-drag', props.r, props.c, true, true); + lastTap = 0; + } else { + // Single tap / Start drag -> Fill + emit('start-drag', props.r, props.c, false, false); + lastTap = now; + } }; const handlePointerUp = (e) => { - if (e.pointerType === 'mouse') return; - clearLongPress(); - if (longPressTriggered) return; - const now = Date.now(); - const isDoubleTap = now - lastTap < 300; - lastTap = now; - if (isDoubleTap) { - emit('start-drag', props.r, props.c, true); - } else { - emit('start-drag', props.r, props.c, false); - } + // Handled in pointerdown }; const handlePointerCancel = (e) => { - if (e.pointerType === 'mouse') return; - clearLongPress(); + // Handled in pointerdown }; diff --git a/src/components/GameBoard.vue b/src/components/GameBoard.vue index f43eef3..a53ff17 100644 --- a/src/components/GameBoard.vue +++ b/src/components/GameBoard.vue @@ -15,6 +15,94 @@ const rowHintsRef = ref(null); const activeRow = ref(null); const activeCol = ref(null); const isFinePointer = ref(false); +const scrollWrapper = ref(null); +const scrollTrack = ref(null); +const showScrollbar = ref(false); +const thumbWidth = ref(20); +const thumbLeft = ref(0); +let isDraggingScroll = false; +let dragStartX = 0; +let dragStartLeft = 0; + +const checkScroll = () => { + const el = scrollWrapper.value; + if (!el) return; + const sw = el.scrollWidth; + const cw = el.clientWidth; + + // Only show custom scrollbar on mobile/tablet (width < 768px) and if content overflows + const isMobile = window.innerWidth <= 768; + showScrollbar.value = isMobile && (sw > cw + 1); + + if (showScrollbar.value) { + // Thumb width percentage = (viewport / total) * 100 + const ratio = cw / sw; + thumbWidth.value = Math.max(10, ratio * 100); + } +}; + +const handleScroll = () => { + if (isDraggingScroll) return; + const el = scrollWrapper.value; + if (!el) return; + const sw = el.scrollWidth; + const cw = el.clientWidth; + const sl = el.scrollLeft; + const maxScroll = sw - cw; + if (maxScroll <= 0) return; + + // Map scroll position to thumb position (0 to 100 - thumbWidth) + const maxThumb = 100 - thumbWidth.value; + thumbLeft.value = (sl / maxScroll) * maxThumb; +}; + +const startScrollDrag = (e) => { + isDraggingScroll = true; + dragStartX = e.clientX || e.touches[0].clientX; + dragStartLeft = thumbLeft.value; + + document.addEventListener('mousemove', onScrollDrag); + document.addEventListener('mouseup', stopScrollDrag); + document.addEventListener('touchmove', onScrollDrag, { passive: false }); + document.addEventListener('touchend', stopScrollDrag); +}; + +const onScrollDrag = (e) => { + if (!isDraggingScroll || !scrollTrack.value) return; + + const clientX = e.clientX || (e.touches ? e.touches[0].clientX : 0); + const deltaX = clientX - dragStartX; + const trackWidth = scrollTrack.value.offsetWidth; + + if (trackWidth === 0) return; + + // Calculate delta as percentage of track + const deltaPercent = (deltaX / trackWidth) * 100; + + // New thumb position + let newLeft = dragStartLeft + deltaPercent; + const maxThumb = 100 - thumbWidth.value; + newLeft = Math.max(0, Math.min(maxThumb, newLeft)); + + thumbLeft.value = newLeft; + + // Sync scroll + const el = scrollWrapper.value; + if (el) { + const sw = el.scrollWidth; + const cw = el.clientWidth; + const maxScroll = sw - cw; + el.scrollLeft = (newLeft / maxThumb) * maxScroll; + } +}; + +const stopScrollDrag = () => { + isDraggingScroll = false; + document.removeEventListener('mousemove', onScrollDrag); + document.removeEventListener('mouseup', stopScrollDrag); + document.removeEventListener('touchmove', onScrollDrag); + document.removeEventListener('touchend', stopScrollDrag); +}; const getRowHintsWidth = () => { const el = rowHintsRef.value?.$el; @@ -74,6 +162,7 @@ onMounted(() => { }); isFinePointer.value = window.matchMedia('(pointer: fine)').matches; window.addEventListener('resize', computeCellSize); + window.addEventListener('resize', checkScroll); window.addEventListener('mouseup', handleGlobalMouseUp); window.addEventListener('pointerup', handleGlobalPointerUp); window.addEventListener('touchend', handleGlobalPointerUp, { passive: true }); @@ -81,6 +170,7 @@ onMounted(() => { onUnmounted(() => { window.removeEventListener('resize', computeCellSize); + window.removeEventListener('resize', checkScroll); window.removeEventListener('mouseup', handleGlobalMouseUp); window.removeEventListener('pointerup', handleGlobalPointerUp); window.removeEventListener('touchend', handleGlobalPointerUp); @@ -89,11 +179,12 @@ onUnmounted(() => { watch(() => store.size, async () => { await nextTick(); computeCellSize(); + checkScroll(); });