Fix Undo icon and improve mobile UX
This commit is contained in:
@@ -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
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="game-board-wrapper">
|
||||
<div class="game-board-wrapper" ref="scrollWrapper" @scroll="handleScroll">
|
||||
<div class="game-container" :style="{ '--cell-size': `${cellSize}px` }">
|
||||
<div class="corner-spacer"></div>
|
||||
|
||||
@@ -131,13 +222,30 @@ watch(() => store.size, async () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<div v-if="showScrollbar" class="fixed-scroll-bar">
|
||||
<div class="fixed-scroll-track" ref="scrollTrack" @pointerdown="startScrollDrag">
|
||||
<div
|
||||
class="fixed-scroll-thumb"
|
||||
:style="{ width: `${thumbWidth}%`, left: `${thumbLeft}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.game-board-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
scrollbar-width: none; /* Hide default scrollbar */
|
||||
}
|
||||
.game-board-wrapper::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
@@ -145,17 +253,24 @@ watch(() => store.size, async () => {
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 0;
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.3);
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
margin-top: 10px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.corner-spacer {
|
||||
height: auto; /* Adapts to Col Hints height */
|
||||
}
|
||||
|
||||
/* Row Hints */
|
||||
.game-container > :nth-child(3) {
|
||||
/* No special styles */
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: var(--gap-size);
|
||||
|
||||
@@ -607,14 +607,14 @@ watch(isMobileMenuOpen, (val) => {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: rgba(10, 10, 25, 0.98);
|
||||
backdrop-filter: blur(20px);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -622,22 +622,24 @@ watch(isMobileMenuOpen, (val) => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid rgba(0, 242, 254, 0.2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-title {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.8rem;
|
||||
color: var(--primary-neon);
|
||||
margin: 0;
|
||||
letter-spacing: 2px;
|
||||
letter-spacing: 3px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.mobile-menu-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.mobile-group {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed } from 'vue';
|
||||
import { usePuzzleStore } from '@/stores/puzzle';
|
||||
import { useTimer } from '@/composables/useTimer';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { RotateCcw, RefreshCw, Eye } from 'lucide-vue-next';
|
||||
import { RotateCcw, RefreshCw, Eye, Undo } from 'lucide-vue-next';
|
||||
|
||||
const store = usePuzzleStore();
|
||||
const { formatTime } = useTimer();
|
||||
@@ -45,7 +45,7 @@ const progressText = computed(() => `${store.progressPercentage.toFixed(3)}%`);
|
||||
</button>
|
||||
<div class="action-separator"></div>
|
||||
<button class="action-btn" @click="store.undo" :title="t('actions.undo')">
|
||||
<RotateCcw :size="20" />
|
||||
<Undo :size="20" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ export function useNonogram() {
|
||||
const dragMode = ref(null); // 'fill', 'empty', 'cross'
|
||||
const startCellState = ref(null);
|
||||
|
||||
const startDrag = (r, c, isRightClick = false) => {
|
||||
const startDrag = (r, c, isRightClick = false, force = false) => {
|
||||
if (store.isGameWon) return;
|
||||
|
||||
isDragging.value = true;
|
||||
@@ -16,9 +16,7 @@ export function useNonogram() {
|
||||
|
||||
if (isRightClick) {
|
||||
// Right click logic
|
||||
// If current is 1 (filled), do nothing usually? Or ignore?
|
||||
// Standard: Toggle 0 <-> 2
|
||||
if (current === 1) {
|
||||
if (!force && current === 1) {
|
||||
dragMode.value = null; // invalid drag start
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import './scrollbar.css';
|
||||
|
||||
:root {
|
||||
/* --- Glassmorphism Design System --- */
|
||||
--bg-gradient: linear-gradient(135deg, #43C6AC 0%, #191654 100%);
|
||||
@@ -52,6 +54,9 @@
|
||||
--scroll-track: rgba(0, 0, 0, 0.1);
|
||||
--scroll-thumb: rgba(255, 255, 255, 0.2);
|
||||
--scroll-thumb-hover: var(--accent-cyan);
|
||||
--fixed-bar-bg: rgba(0, 0, 0, 0.85);
|
||||
--fixed-bar-thumb: rgba(0, 242, 255, 0.5);
|
||||
--fixed-bar-thumb-hover: rgba(0, 242, 255, 0.8);
|
||||
|
||||
/* Rozmiary */
|
||||
--cell-size: 30px;
|
||||
@@ -113,6 +118,9 @@
|
||||
--scroll-track: rgba(15, 23, 42, 0.08);
|
||||
--scroll-thumb: rgba(15, 23, 42, 0.2);
|
||||
--scroll-thumb-hover: #0ea5e9;
|
||||
--fixed-bar-bg: rgba(248, 250, 255, 0.95);
|
||||
--fixed-bar-thumb: rgba(14, 165, 233, 0.5);
|
||||
--fixed-bar-thumb-hover: rgba(14, 165, 233, 0.8);
|
||||
}
|
||||
|
||||
* {
|
||||
|
||||
54
src/styles/scrollbar.css
Normal file
54
src/styles/scrollbar.css
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
/* Fixed Scroll Bar */
|
||||
.fixed-scroll-bar {
|
||||
position: fixed;
|
||||
bottom: 15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 85%;
|
||||
max-width: 400px;
|
||||
height: 44px; /* Increased hit area */
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(10px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
|
||||
.fixed-scroll-track {
|
||||
width: 100%;
|
||||
height: 14px; /* Increased visual thickness */
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
border-radius: 7px;
|
||||
position: relative;
|
||||
pointer-events: auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.fixed-scroll-thumb {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: var(--primary-accent);
|
||||
border-radius: 7px;
|
||||
cursor: grab;
|
||||
box-shadow: 0 0 12px rgba(0, 242, 255, 0.5);
|
||||
transition: background 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.fixed-scroll-thumb:active {
|
||||
cursor: grabbing;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 15px rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
Reference in New Issue
Block a user