Mobile-first i pixel-perfect: wspólne --cell-size, grid dla podpowiedzi, dynamiczny rozmiar komórki z uwzględnieniem paddingów i szerokości opisów; poprawa touch/pointer i double-tap na mobile; wyrównanie layoutu bez nachodzenia na desktopie

This commit is contained in:
2026-02-08 15:02:59 +01:00
parent ec1cf89ee5
commit 657dc9cc1f
5 changed files with 145 additions and 24 deletions

View File

@@ -111,4 +111,22 @@ h1 {
justify-content: center; justify-content: center;
margin-top: 10px; margin-top: 10px;
} }
@media (max-width: 768px) {
.game-header {
margin-top: 20px;
margin-bottom: 20px;
}
h1 {
font-size: 2.4rem;
letter-spacing: 3px;
}
}
@media (max-width: 420px) {
h1 {
font-size: 2rem;
letter-spacing: 2px;
}
}
</style> </style>

View File

@@ -21,10 +21,22 @@ const cellClass = computed(() => {
} }
}); });
const handleMouseDown = (e) => { let lastTap = 0;
// 0 = left, 2 = right
const handlePointerDown = (e) => {
if (e.pointerType === 'mouse') {
if (e.button === 0) emit('start-drag', props.r, props.c, false); 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 === 2) emit('start-drag', props.r, props.c, true);
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);
}
}; };
</script> </script>
@@ -32,7 +44,9 @@ const handleMouseDown = (e) => {
<div <div
class="cell" class="cell"
:class="cellClass" :class="cellClass"
@mousedown.prevent="handleMouseDown" :data-r="props.r"
:data-c="props.c"
@pointerdown.prevent="handlePointerDown"
@mouseenter="emit('enter-cell', props.r, props.c)" @mouseenter="emit('enter-cell', props.r, props.c)"
@contextmenu.prevent @contextmenu.prevent
> >
@@ -52,6 +66,7 @@ const handleMouseDown = (e) => {
align-items: center; align-items: center;
transition: background-color 0.1s ease, box-shadow 0.1s ease; transition: background-color 0.1s ease, box-shadow 0.1s ease;
user-select: none; user-select: none;
touch-action: none;
} }
.cell:hover { .cell:hover {

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, onUnmounted, computed } from 'vue'; import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue';
import { usePuzzleStore } from '@/stores/puzzle'; import { usePuzzleStore } from '@/stores/puzzle';
import { useHints } from '@/composables/useHints'; import { useHints } from '@/composables/useHints';
import { useNonogram } from '@/composables/useNonogram'; import { useNonogram } from '@/composables/useNonogram';
@@ -10,30 +10,81 @@ const store = usePuzzleStore();
const { rowHints, colHints } = useHints(computed(() => store.solution)); const { rowHints, colHints } = useHints(computed(() => store.solution));
const { startDrag, onMouseEnter, stopDrag } = useNonogram(); const { startDrag, onMouseEnter, stopDrag } = useNonogram();
// Global mouseup to stop dragging even if mouse leaves grid const cellSize = ref(30);
const rowHintsRef = ref(null);
const getRowHintsWidth = () => {
const el = rowHintsRef.value?.$el;
if (!el) return 0;
return el.offsetWidth || 0;
};
const computeCellSize = () => {
const vw = Math.min(window.innerWidth, 900);
const rootStyles = getComputedStyle(document.documentElement);
const hintWidth = getRowHintsWidth();
const gapRaw = rootStyles.getPropertyValue('--gap-size') || '2px';
const gridPadRaw = rootStyles.getPropertyValue('--grid-padding') || '5px';
const gap = parseFloat(gapRaw);
const gridPad = parseFloat(gridPadRaw);
const bodyStyles = getComputedStyle(document.body);
const bodyPadding = parseFloat(bodyStyles.paddingLeft) + parseFloat(bodyStyles.paddingRight);
const availableForGrid = vw - bodyPadding - hintWidth;
const size = Math.floor((availableForGrid - gridPad * 2 - (store.size - 1) * gap) / store.size);
cellSize.value = Math.max(18, Math.min(36, size));
};
const handleGlobalMouseUp = () => { const handleGlobalMouseUp = () => {
stopDrag(); stopDrag();
};
const handleGlobalPointerUp = () => {
stopDrag();
};
const handlePointerMove = (e) => {
const el = document.elementFromPoint(e.clientX, e.clientY);
if (!el) return;
const r = el.getAttribute('data-r');
const c = el.getAttribute('data-c');
if (r != null && c != null) {
onMouseEnter(Number(r), Number(c));
}
}; };
onMounted(() => { onMounted(() => {
window.addEventListener('mouseup', handleGlobalMouseUp); nextTick(() => {
computeCellSize();
});
window.addEventListener('resize', computeCellSize);
window.addEventListener('mouseup', handleGlobalMouseUp);
window.addEventListener('pointerup', handleGlobalPointerUp);
window.addEventListener('touchend', handleGlobalPointerUp, { passive: true });
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('mouseup', handleGlobalMouseUp); window.removeEventListener('resize', computeCellSize);
window.removeEventListener('mouseup', handleGlobalMouseUp);
window.removeEventListener('pointerup', handleGlobalPointerUp);
window.removeEventListener('touchend', handleGlobalPointerUp);
});
watch(() => store.size, async () => {
await nextTick();
computeCellSize();
}); });
</script> </script>
<template> <template>
<div class="game-board-wrapper"> <div class="game-board-wrapper">
<div class="game-container"> <div class="game-container" :style="{ '--cell-size': `${cellSize}px` }">
<div class="corner-spacer"></div> <div class="corner-spacer"></div>
<!-- Column Hints --> <!-- Column Hints -->
<Hints :hints="colHints" orientation="col" /> <Hints :hints="colHints" orientation="col" :size="store.size" />
<!-- Row Hints --> <!-- Row Hints -->
<Hints :hints="rowHints" orientation="row" /> <Hints ref="rowHintsRef" :hints="rowHints" orientation="row" :size="store.size" />
<!-- Grid --> <!-- Grid -->
<div <div
@@ -42,6 +93,7 @@ onUnmounted(() => {
gridTemplateColumns: `repeat(${store.size}, var(--cell-size))`, gridTemplateColumns: `repeat(${store.size}, var(--cell-size))`,
gridTemplateRows: `repeat(${store.size}, var(--cell-size))` gridTemplateRows: `repeat(${store.size}, var(--cell-size))`
}" }"
@pointermove.prevent="handlePointerMove"
@mouseleave="stopDrag" @mouseleave="stopDrag"
> >
<template v-for="(row, r) in store.playerGrid" :key="r"> <template v-for="(row, r) in store.playerGrid" :key="r">
@@ -84,14 +136,13 @@ onUnmounted(() => {
} }
.corner-spacer { .corner-spacer {
width: 100px; /* Must match Row Hints width */
height: auto; /* Adapts to Col Hints height */ height: auto; /* Adapts to Col Hints height */
} }
.grid { .grid {
display: grid; display: grid;
gap: var(--gap-size); gap: var(--gap-size);
padding: 5px; padding: var(--grid-padding);
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-radius: 4px; border-radius: 4px;
} }

View File

@@ -8,12 +8,22 @@ defineProps({
type: String, type: String,
required: true, required: true,
validator: (v) => ['row', 'col'].includes(v) validator: (v) => ['row', 'col'].includes(v)
},
size: {
type: Number,
required: true
} }
}); });
</script> </script>
<template> <template>
<div class="hints-container" :class="orientation"> <div
class="hints-container"
:class="orientation"
:style="orientation === 'col'
? { gridTemplateColumns: `repeat(${size}, var(--cell-size))` }
: { gridTemplateRows: `repeat(${size}, var(--cell-size))` }"
>
<div <div
v-for="(group, index) in hints" v-for="(group, index) in hints"
:key="index" :key="index"
@@ -33,22 +43,21 @@ defineProps({
<style scoped> <style scoped>
.hints-container { .hints-container {
display: flex; display: grid;
gap: var(--gap-size); gap: var(--gap-size);
} }
.hints-container.col { .hints-container.col {
flex-direction: row; padding-bottom: var(--grid-padding);
margin-bottom: 5px;
align-items: flex-end; align-items: flex-end;
padding: 0 5px; /* Match grid padding */ padding-left: var(--grid-padding);
padding-right: var(--grid-padding);
} }
.hints-container.row { .hints-container.row {
flex-direction: column;
margin-right: 5px;
align-items: flex-end; align-items: flex-end;
padding: 5px 0; /* Match grid padding */ padding: var(--grid-padding) var(--grid-padding) var(--grid-padding) 0;
width: max-content;
} }
.hint-group { .hint-group {
@@ -59,20 +68,19 @@ defineProps({
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px; border-radius: 4px;
transition: all 0.3s ease; transition: all 0.3s ease;
width: 100%;
height: 100%;
} }
.col .hint-group { .col .hint-group {
flex-direction: column; flex-direction: column;
width: var(--cell-size);
padding: 4px 2px; padding: 4px 2px;
justify-content: flex-end; justify-content: flex-end;
} }
.row .hint-group { .row .hint-group {
flex-direction: row; flex-direction: row;
height: var(--cell-size);
padding: 2px 8px; padding: 2px 8px;
width: 100px; /* Stała szerokość */
} }
.hint-num { .hint-num {

View File

@@ -15,6 +15,8 @@
/* Rozmiary */ /* Rozmiary */
--cell-size: 30px; --cell-size: 30px;
--gap-size: 2px; --gap-size: 2px;
--hint-row-width: 100px;
--grid-padding: 5px;
} }
* { * {
@@ -119,3 +121,30 @@ button.btn-neon.secondary {
.fade-leave-to { .fade-leave-to {
opacity: 0; opacity: 0;
} }
@media (max-width: 768px) {
:root {
--gap-size: 1px;
--hint-row-width: 72px;
}
body {
padding: 12px;
}
button.btn-neon {
padding: 10px 18px;
font-size: 0.9rem;
}
}
@media (max-width: 420px) {
:root {
--hint-row-width: 60px;
}
body {
padding: 8px;
}
button.btn-neon {
padding: 9px 16px;
font-size: 0.85rem;
}
}