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:
18
src/App.vue
18
src/App.vue
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
computeCellSize();
|
||||||
|
});
|
||||||
|
window.addEventListener('resize', computeCellSize);
|
||||||
window.addEventListener('mouseup', handleGlobalMouseUp);
|
window.addEventListener('mouseup', handleGlobalMouseUp);
|
||||||
|
window.addEventListener('pointerup', handleGlobalPointerUp);
|
||||||
|
window.addEventListener('touchend', handleGlobalPointerUp, { passive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', computeCellSize);
|
||||||
window.removeEventListener('mouseup', handleGlobalMouseUp);
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user