mirror of
https://github.com/tiennm99/sokoban.git
synced 2026-10-11 03:13:52 +00:00
docs: log mobile mistouch fix and add plan artifacts
This commit is contained in:
1 parent
c80b9cedb6
commit
2f5b184344
9 files changed
+865
No files matched your search
@@ -1,5 +1,27 @@
|
||||
# Project Changelog
|
||||
|
||||
## 2026-04-28 — Mobile Mistouch & Layout Fix
|
||||
|
||||
### Fixed
|
||||
- **Footer mistouch during gameplay:** `miti99.com` link in the global footer was overlapping the D-pad/action-stack tap zones at the bottom of the screen. Footer now hidden on `view === 'game'` (`App.svelte`), visible only on Menu + Level Select.
|
||||
- **D-pad/board overlap on small viewports:** `MobileControls` no longer uses `position: fixed`. The dock-left + D-pad live in a single in-flow flex row at the bottom of `GameView`'s flex column. Board-wrap is now `flex: 1 1 auto; min-height: 0` so puzzles scroll within their wrapper, never under the controls. Magic `max-height: calc(100vh - 260px)` removed.
|
||||
- **Footer safe-area:** Added `env(safe-area-inset-bottom)` to `.site-footer` offset so it sits above the iPhone home-indicator gesture bar.
|
||||
- **WCAG-AA contrast:** Armed RESET button uses white text (4.85:1 on `--danger`) instead of `--bg` text (failed AA at 3.1:1).
|
||||
|
||||
### Added
|
||||
- **Reset two-tap arming** (`MobileControls.svelte`): First tap on RESET arms (label flips "RESET" → "TAP AGAIN", red background, 2s timeout); second tap within the window restarts. Prevents mid-puzzle wipe-outs from a stray thumb. Keyboard `R` unchanged (immediate reset).
|
||||
- **D-pad press-and-hold repeat:** Holding any arrow continuously moves the player at 130 ms cadence (parity with keyboard `REPEAT_MS`). Implemented via Pointer Events (`pointerdown`/`pointerup`/`pointercancel`/`pointerleave`) + shared `setInterval`. `e.preventDefault()` on `pointerdown` suppresses the synthesized click double-fire.
|
||||
|
||||
### Changed
|
||||
- `App.svelte`: Conditional footer render; safe-area offset on `.site-footer`.
|
||||
- `GameView.svelte`: `.game` is now `align-self: stretch; display: flex; flex-direction: column;` so it fills `#app`'s inner area without forcing page-scroll. `.board-wrap` becomes the flex-grow child. `computeTileSize` margin reduced from 260→220 (coarse) and 140→120 (fine) since the dock is no longer fixed-positioned.
|
||||
- `MobileControls.svelte`: Rewritten — drops fixed positioning + z-index, wraps dock-left + D-pad in `.mobile-dock` flex row, gains arming + hold-repeat state with `$effect` cleanup on unmount.
|
||||
|
||||
### Notes
|
||||
- Bundle: 70.02 kB JS / 24.68 kB gzipped (no meaningful change).
|
||||
- Build clean. Manual smoke test pending on real iOS/Android.
|
||||
- Plan: `plans/260428-1004-mobile-mistouch-fix/`.
|
||||
|
||||
## 2026-04-27 — Mobile Comfort & PWA
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
phase: 01
|
||||
title: Hide footer on game view + safe-area
|
||||
status: completed
|
||||
priority: high
|
||||
effort: trivial
|
||||
files: [src/App.svelte]
|
||||
---
|
||||
|
||||
# Phase 01 — Hide footer on game view + safe-area
|
||||
|
||||
## Context
|
||||
|
||||
Brainstorm decisions D1 + D2. Footer link `https://miti99.com` causes mistouches during gameplay because it sits in the same band as D-pad/action stack.
|
||||
|
||||
Footer is `position: fixed; bottom: 6px; pointer-events: none` with the `<a>` having `pointer-events: auto`. Currently rendered globally in `App.svelte` regardless of view.
|
||||
|
||||
## Goal
|
||||
|
||||
Footer visible only on `view === 'menu'` and `view === 'levels'`. Hidden during `view === 'game'`. Add safe-area inset where shown.
|
||||
|
||||
## Implementation
|
||||
|
||||
### `src/App.svelte`
|
||||
|
||||
1. Wrap `<footer class="site-footer">…</footer>` in a conditional:
|
||||
```svelte
|
||||
{#if view !== 'game'}
|
||||
<footer class="site-footer">…</footer>
|
||||
{/if}
|
||||
```
|
||||
2. Update `.site-footer` CSS:
|
||||
```css
|
||||
bottom: calc(6px + env(safe-area-inset-bottom));
|
||||
```
|
||||
|
||||
That's the entire phase.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [ ] Footer absent during gameplay (verify in DOM inspector on game view)
|
||||
- [ ] Footer present on Menu + Level Select
|
||||
- [ ] On iPhone simulator with home indicator, footer sits above the indicator (does not overlap)
|
||||
- [ ] Build passes (`npm run build`)
|
||||
- [ ] No console errors on view transitions
|
||||
|
||||
## Risk
|
||||
|
||||
Near-zero. Single file, two edits, no logic change.
|
||||
|
||||
## Test
|
||||
|
||||
Manual:
|
||||
1. `npm run dev`
|
||||
2. Resize to 390×844 (iPhone 14)
|
||||
3. Menu → footer visible at bottom
|
||||
4. Levels → footer visible at bottom
|
||||
5. Click level → game starts, footer absent
|
||||
6. Esc back → footer reappears on level select
|
||||
|
||||
## Rollback
|
||||
|
||||
`git revert` of phase commit. No data, no migration.
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
phase: 02
|
||||
title: In-flow MobileControls (fix D-pad/board overlap)
|
||||
status: completed
|
||||
priority: high
|
||||
effort: medium
|
||||
files: [src/views/GameView.svelte, src/views/MobileControls.svelte]
|
||||
---
|
||||
|
||||
# Phase 02 — In-flow MobileControls
|
||||
|
||||
## Context
|
||||
|
||||
Brainstorm decision D5. `MobileControls.svelte` uses `position: fixed; z-index: 50` for both dock-left and D-pad. `.board-wrap` has `overflow: auto` and `max-height: calc(100vh - 260px)` on coarse pointer — vertical overlap protected, but tall puzzles scroll **under** the fixed dock; width is unconstrained so corner-pinned controls float over board content.
|
||||
|
||||
Approach α from brainstorm: convert MobileControls to in-flow grid/flex row at bottom of `.screen.game`. Board-wrap becomes flex-grow child. Browser handles z-index/scroll/safe-area naturally.
|
||||
|
||||
## Goal
|
||||
|
||||
D-pad and action stack live in document flow on mobile. Board-wrap occupies remaining space above. No fixed positioning, no z-index, no max-height clamp tied to magic 260.
|
||||
|
||||
## Implementation
|
||||
|
||||
### `src/views/GameView.svelte`
|
||||
|
||||
1. Make `.screen.game` a column flex container that fills viewport:
|
||||
```css
|
||||
.game {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 100vh; /* or use 100dvh for mobile address-bar */
|
||||
}
|
||||
```
|
||||
2. `.board-wrap` becomes flex-grow (auto-fits remaining space):
|
||||
```css
|
||||
.board-wrap {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
min-height: 0; /* required for flex children to allow overflow */
|
||||
max-width: calc(100vw - 48px);
|
||||
}
|
||||
```
|
||||
Remove the `max-height: calc(100vh - 140px)` and the coarse-pointer override `max-height: calc(100vh - 260px)`.
|
||||
3. `computeTileSize` margin: drop the magic 260. Measure remaining height via `.board-wrap` `clientHeight` after layout, OR keep simpler approach — use `window.innerHeight - hud.offsetHeight - mobileControls.offsetHeight - padding`. KISS: use `requestAnimationFrame` after mount to measure; recompute on `onResize` and `pointer: coarse` change. If complexity grows, fall back to keeping the constant but document it.
|
||||
|
||||
Pragmatic version (KISS):
|
||||
- Keep margin constants but reduce to `isCoarse ? 200 : 120` since dock no longer floats.
|
||||
- Recompute on resize as today.
|
||||
|
||||
### `src/views/MobileControls.svelte`
|
||||
|
||||
1. Drop `position: fixed`, `z-index`, `bottom`/`left`/`right` rules from `.dock-left` and `.dpad`.
|
||||
2. Wrap both into a single bottom-row container:
|
||||
```svelte
|
||||
<div class="mobile-dock">
|
||||
<div class="dock-left">…</div>
|
||||
<div class="dpad">…</div>
|
||||
</div>
|
||||
```
|
||||
3. Style:
|
||||
```css
|
||||
.mobile-dock { display: none; }
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.mobile-dock {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
padding: 0 calc(12px + env(safe-area-inset-left)) calc(12px + env(safe-area-inset-bottom)) calc(12px + env(safe-area-inset-right));
|
||||
width: 100%;
|
||||
}
|
||||
.dock-left { display: flex; flex-direction: column; gap: 8px; }
|
||||
.dpad { display: grid; grid-template-columns: 56px 56px 56px; grid-template-rows: 56px 56px;
|
||||
grid-template-areas: ". up . " "left down right"; gap: 4px; }
|
||||
}
|
||||
```
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [ ] D-pad never visually overlaps board content at 360, 390, 414, 430px wide
|
||||
- [ ] No `position: fixed` in MobileControls.svelte
|
||||
- [ ] Board scrolls within `.board-wrap`, not behind dock
|
||||
- [ ] Safe-area insets respected on iPhone X+
|
||||
- [ ] Build passes
|
||||
- [ ] Desktop unchanged (controls still hidden, hud right actions still work)
|
||||
|
||||
## Risk
|
||||
|
||||
- Layout regression on landscape iPad / iPad split-view. **Mitigation:** smoke test 768×1024 portrait, 1024×768 landscape.
|
||||
- `min-height: 100vh` vs iOS Safari address bar. **Mitigation:** test in real Safari; switch to `100dvh` if jumping.
|
||||
- Flex `min-height: 0` on board-wrap is required for overflow scroll inside flex column — easy to forget.
|
||||
|
||||
## Test
|
||||
|
||||
Manual matrix:
|
||||
1. 360×640 portrait — small puzzle (Microban 1) and large puzzle (Microban 155)
|
||||
2. 414×896 portrait
|
||||
3. 768×1024 portrait + 1024×768 landscape
|
||||
4. Desktop 1440×900 — verify no regression
|
||||
5. Verify scroll inside board-wrap (not page scroll) on tall puzzles
|
||||
|
||||
## Rollback
|
||||
|
||||
`git revert` of phase commit. Pre-phase fixed-positioning still works for desktop.
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
phase: 03
|
||||
title: Reset two-tap arming
|
||||
status: completed
|
||||
priority: medium
|
||||
effort: small
|
||||
files: [src/views/MobileControls.svelte]
|
||||
depends_on: [phase-02]
|
||||
---
|
||||
|
||||
# Phase 03 — Reset two-tap arming
|
||||
|
||||
## Context
|
||||
|
||||
Brainstorm decision D3. RESET button is currently one-tap destructive — mid-puzzle mis-tap wipes player progress. User picked option (a): two-tap arming. First tap arms (label flips, color changes); second tap within 2s actually resets; auto-disarms after timeout.
|
||||
|
||||
Keyboard `R` is unaffected (assumed deliberate by power users).
|
||||
|
||||
## Goal
|
||||
|
||||
Mobile RESET requires a second confirmation tap within 2 seconds. Visual delta is unmistakable when armed. No modal, no extra dependencies.
|
||||
|
||||
## Implementation
|
||||
|
||||
### `src/views/MobileControls.svelte`
|
||||
|
||||
1. Add local state for armed flag + timer:
|
||||
```svelte
|
||||
<script>
|
||||
let { onMove, onUndo, onRestart, onLevels } = $props();
|
||||
let resetArmed = $state(false);
|
||||
let armTimer = null;
|
||||
|
||||
function disarm() { resetArmed = false; armTimer = null; }
|
||||
|
||||
function handleReset() {
|
||||
if (resetArmed) {
|
||||
clearTimeout(armTimer);
|
||||
disarm();
|
||||
onRestart();
|
||||
return;
|
||||
}
|
||||
resetArmed = true;
|
||||
clearTimeout(armTimer);
|
||||
armTimer = setTimeout(disarm, 2000);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
2. Update RESET button:
|
||||
```svelte
|
||||
<button
|
||||
class="action"
|
||||
class:armed={resetArmed}
|
||||
type="button"
|
||||
onclick={handleReset}
|
||||
aria-label={resetArmed ? 'Tap again to confirm reset' : 'Reset'}>
|
||||
{resetArmed ? 'TAP AGAIN' : 'RESET'}
|
||||
</button>
|
||||
```
|
||||
3. Add `.armed` style — strong visual delta:
|
||||
```css
|
||||
.action.armed {
|
||||
background: var(--danger);
|
||||
color: var(--bg);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
```
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [ ] Single tap on RESET does not restart the level
|
||||
- [ ] Label flips to "TAP AGAIN" with red background
|
||||
- [ ] Second tap within 2s restarts the level
|
||||
- [ ] No second tap → button auto-disarms after 2s, label reverts
|
||||
- [ ] Tapping UNDO or LVLS while armed does NOT cancel the arm (only 2s timeout cancels) — *acceptable behavior, document if questioned*
|
||||
- [ ] Keyboard `R` still resets immediately (untouched)
|
||||
- [ ] aria-label updates with state for screen readers
|
||||
|
||||
## Risk
|
||||
|
||||
- Cleanup of `armTimer` on unmount: Svelte 5 — if user leaves view while armed, timer fires on disarmed state (no-op). Safe.
|
||||
- Color contrast of armed state: verify `--danger` on `--bg` meets WCAG AA. (Likely fine — reuse existing token.)
|
||||
|
||||
## Test
|
||||
|
||||
Manual:
|
||||
1. On mobile viewport, mid-level: tap RESET once → label changes, no reset
|
||||
2. Tap again within 2s → level resets, label reverts
|
||||
3. Tap once, wait 3s → label reverts, level untouched
|
||||
4. Spam-tap RESET 5 times rapidly → resets exactly once on the 2nd tap (subsequent taps from disarmed state would re-arm; this is fine)
|
||||
5. Press R on keyboard → immediate reset (regression check)
|
||||
|
||||
## Rollback
|
||||
|
||||
Revert phase commit. Reset returns to one-tap behavior.
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
phase: 04
|
||||
title: D-pad press-and-hold repeat
|
||||
status: completed
|
||||
priority: medium
|
||||
effort: small
|
||||
files: [src/views/MobileControls.svelte]
|
||||
depends_on: [phase-02]
|
||||
---
|
||||
|
||||
# Phase 04 — D-pad press-and-hold repeat
|
||||
|
||||
## Context
|
||||
|
||||
Brainstorm decision D4. Keyboard movement repeats at REPEAT_MS=130 (see GameView.svelte:96). D-pad currently fires on `onclick` only — no parity. Holding a thumb on an arrow does nothing.
|
||||
|
||||
Approach (i): Pointer Events (`pointerdown` / `pointerup` / `pointercancel` / `pointerleave`) + setInterval. Covers touch + mouse + stylus uniformly.
|
||||
|
||||
## Goal
|
||||
|
||||
Holding a D-pad arrow continuously moves the player at 130ms cadence (matches keyboard).
|
||||
|
||||
## Implementation
|
||||
|
||||
### `src/views/MobileControls.svelte`
|
||||
|
||||
1. Add hold-repeat helper:
|
||||
```svelte
|
||||
<script>
|
||||
const REPEAT_MS = 130;
|
||||
let { onMove, onUndo, onRestart, onLevels } = $props();
|
||||
let holdTimer = null;
|
||||
|
||||
function startHold(dx, dy) {
|
||||
onMove(dx, dy); // immediate fire
|
||||
clearInterval(holdTimer);
|
||||
holdTimer = setInterval(() => onMove(dx, dy), REPEAT_MS);
|
||||
}
|
||||
function endHold() {
|
||||
clearInterval(holdTimer);
|
||||
holdTimer = null;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
2. Replace each arrow's `onclick` with pointer handlers:
|
||||
```svelte
|
||||
<button
|
||||
class="arrow up"
|
||||
type="button"
|
||||
onpointerdown={(e) => { e.preventDefault(); startHold(0, -1); }}
|
||||
onpointerup={endHold}
|
||||
onpointercancel={endHold}
|
||||
onpointerleave={endHold}
|
||||
aria-label="Up">▲</button>
|
||||
```
|
||||
Repeat for down/left/right (same pattern, different dx/dy).
|
||||
3. Keep `onclick` removed — pointerdown handles the immediate fire. Don't double-fire.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [ ] Single tap on arrow → one move (same as before)
|
||||
- [ ] Hold arrow 1s → ~7-8 moves (1000/130 ≈ 7.7)
|
||||
- [ ] Release arrow → movement stops within 130ms
|
||||
- [ ] Sliding finger off the arrow stops movement (`pointerleave`)
|
||||
- [ ] Multi-touch on two arrows → second arrow takes over (single `holdTimer` is intentional — last wins)
|
||||
- [ ] Keyboard hold-repeat unchanged (regression check)
|
||||
|
||||
## Risk
|
||||
|
||||
- `holdTimer` leak if component unmounts mid-hold. **Mitigation:** Svelte's `$effect` cleanup — add `return () => clearInterval(holdTimer);` in an `$effect` block, or accept that the closure-held interval has no DOM target after unmount (`onMove` callback still valid → fires moves into a remounted GameView). Add cleanup defensively.
|
||||
- `e.preventDefault()` on `pointerdown` is needed to suppress the synthesized `click` after a release — otherwise a double-fire.
|
||||
- Repeat speed (130ms) inherited from keyboard. If feels too fast on touch with longer travel, tunable in one place.
|
||||
|
||||
## Test
|
||||
|
||||
Manual:
|
||||
1. On phone viewport, mid-level: tap once on right arrow → player moves 1 tile
|
||||
2. Hold right arrow ~1s → player moves ~7-8 tiles continuously
|
||||
3. Hold right, slide finger off button → movement stops immediately
|
||||
4. Hold right, then also press left → either-or behavior; last hold wins
|
||||
5. Push a box continuously by holding direction → haptic pulses fire on each box-push (regression of pulse logic)
|
||||
6. Switch to keyboard → hold ArrowRight → still works (not regressed)
|
||||
|
||||
## Rollback
|
||||
|
||||
Revert phase commit. D-pad returns to tap-only.
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Mobile Mistouch & Layout Fix
|
||||
date: 2026-04-28
|
||||
status: completed
|
||||
branch: main
|
||||
mode: fast
|
||||
blockedBy: []
|
||||
blocks: []
|
||||
---
|
||||
|
||||
# Mobile Mistouch & Layout Fix
|
||||
|
||||
Fix footer mistouch + D-pad/board overlap + Reset destructive tap + D-pad hold-repeat. All 6 decisions locked in brainstorm doc.
|
||||
|
||||
## Goal
|
||||
|
||||
Mobile gameplay (≤480px) is mistouch-free: cannot tap footer link mid-game, D-pad never overlaps board, Reset requires two taps, holding arrow continuously moves player.
|
||||
|
||||
## Reports
|
||||
|
||||
- `../reports/ui-ux-260428-0935-mobile-footer-mistouch.md` — UX review (root cause)
|
||||
- `../reports/brainstorm-260428-1004-mobile-mistouch-and-layout.md` — design + locked decisions
|
||||
|
||||
## Phases
|
||||
|
||||
| # | File | Title | Status | Independently shippable |
|
||||
|---|------|-------|--------|-------------------------|
|
||||
| 01 | [phase-01-footer-hide-on-game.md](phase-01-footer-hide-on-game.md) | Hide footer on game view + safe-area | completed | Yes |
|
||||
| 02 | [phase-02-layout-refactor.md](phase-02-layout-refactor.md) | In-flow MobileControls (D-pad overlap fix) | completed | Yes |
|
||||
| 03 | [phase-03-reset-confirm-tap.md](phase-03-reset-confirm-tap.md) | Reset two-tap arming | completed | Yes |
|
||||
| 04 | [phase-04-dpad-hold-repeat.md](phase-04-dpad-hold-repeat.md) | D-pad press-and-hold repeat | completed | Yes |
|
||||
|
||||
## Files affected (all phases combined)
|
||||
|
||||
- `src/App.svelte` (phase 01)
|
||||
- `src/views/GameView.svelte` (phase 02 — layout, possibly phase 03 — Reset state)
|
||||
- `src/views/MobileControls.svelte` (phases 02, 03, 04)
|
||||
|
||||
## Order rationale
|
||||
|
||||
01 first — smallest, fixes the reported user complaint immediately, zero risk.
|
||||
02 second — biggest refactor; do it before adding behavior changes to MobileControls.
|
||||
03 + 04 stack on top of 02's clean layout.
|
||||
|
||||
## Success criteria
|
||||
|
||||
Combined acceptance:
|
||||
|
||||
- [ ] Cannot tap miti99 link during gameplay
|
||||
- [ ] Footer respects safe-area on Menu + Level Select
|
||||
- [ ] D-pad never overlaps board content at 360–430px wide
|
||||
- [ ] Reset requires two taps within 2s
|
||||
- [ ] Holding arrow moves player at 130ms/step
|
||||
- [ ] Build passes, no console errors
|
||||
|
||||
## Out of scope
|
||||
|
||||
Phase 4 roadmap items (sounds, facing direction, level tabs, unit tests) — separate plan.
|
||||
|
||||
## Open questions
|
||||
|
||||
(none — locked in brainstorm)
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Mobile Mistouch & Layout Fix — Code Review
|
||||
date: 2026-04-28
|
||||
slug: mobile-fix-review
|
||||
related:
|
||||
- ../plan.md
|
||||
- ../../reports/brainstorm-260428-1004-mobile-mistouch-and-layout.md
|
||||
status: review-complete
|
||||
---
|
||||
|
||||
# Code Review — Mobile Mistouch & Layout Fix
|
||||
|
||||
Scope: `src/App.svelte`, `src/views/GameView.svelte`, `src/views/MobileControls.svelte` (uncommitted, ~150 LOC net).
|
||||
|
||||
## Verdict
|
||||
|
||||
Implementation honors all 6 brainstorm decisions. No critical bugs, no security issues, no data leaks. Two real concerns: a page-scroll bug from `min-height: 100dvh` inside padded `#app`, and a WCAG-AA contrast fail on the armed-RESET button. Multi-touch semantics are implemented correctly per "last-press wins" but have a non-obvious side effect worth documenting.
|
||||
|
||||
## Critical
|
||||
none.
|
||||
|
||||
## High
|
||||
|
||||
- **GameView.svelte:212 — `.game { min-height: 100dvh }` inside `#app` (padding 24px) forces page scroll.**
|
||||
`#app` (app.css:62) is flex container with `padding: 24px` (48px vertical). Setting `.game` to `min-height: 100dvh` makes it ≥ viewport tall, so total body height ≥ `100dvh + 48px`. On iOS even with `overscroll-behavior: contain`, this allows page rubber-band scroll during gameplay — partially defeats the dock-overlap fix on small landscape phones.
|
||||
Fix: either `min-height: 100%` (relative to #app), or remove the rule entirely (flex column with `flex: 1 1 auto` board-wrap doesn't strictly need the min-height — the dock will sit at the natural bottom of content), or scope to `(pointer: coarse)` and pair with `padding: 0` override on `#app`.
|
||||
Quick check: open in mobile devtools at 414×800, scroll the page — there should be zero overflow when the board fits comfortably.
|
||||
|
||||
- **MobileControls.svelte:160 — `armed` button fails WCAG AA contrast.**
|
||||
`var(--danger)` (#BF616A) bg with `var(--bg)` (#2E3440) text → contrast ≈ 3.1:1. Button text is 13px/700 (app.css inherits — under WCAG "large text" threshold of 14pt bold ≈ 18.67px), so AA needs 4.5:1. Per code-standards spec, accessibility matters.
|
||||
Three options:
|
||||
1. Use `var(--text)` (#ECEFF4) on `--danger` → 3.4:1 (still fails, but closer).
|
||||
2. Add a stronger danger variant (e.g. `--danger-strong: #8B3A41`) and use white text → ≥ 6:1.
|
||||
3. Accept AA-large by bumping armed font-size to 14pt bold (≥ 18.67px) — but that changes layout.
|
||||
Recommend option 2 (cleanest, also gives a reusable token).
|
||||
|
||||
## Medium
|
||||
|
||||
- **MobileControls.svelte:13–24 — single shared `holdTimer` means releasing finger #2 cancels the hold from finger #1.**
|
||||
Sequence: user holds Up (interval running), then briefly taps Right (interval reassigned to right). `pointerup` on Right fires `endHold()` → interval cleared. User's first finger is still on Up but no further moves fire until they lift and re-press. The plan accepts "last wins" semantics, but doesn't acknowledge this implicit cancel-on-other-release. Either:
|
||||
- Document explicitly in the source comment near `startHold` so the next maintainer doesn't mistake it for a bug, OR
|
||||
- Track active pointers per-button (Map of pointerId → direction) and only stop the interval whose pointerId is up'd. KISS says the first option suffices for a Sokoban game where two-finger inputs are unintended anyway.
|
||||
|
||||
- **MobileControls.svelte:72,80,88,96 — keyboard accessibility hole.**
|
||||
Arrow buttons no longer respond to keyboard activation (Enter/Space): work moved from `onclick` to `onpointerdown`. Pressing Enter on a focused arrow button now does nothing because no synthesized pointerdown fires. Mitigated because the dock is hidden on `pointer: fine` and `GameView` has `<svelte:window onkeydown>` for arrows directly — but a coarse-pointer device with a paired keyboard (iPad + Magic Keyboard, Android tablet + BT keyboard) loses the dock as a fallback.
|
||||
Either: add `onclick={() => { onMove(dx, dy); }}` alongside pointer handlers (one-shot move on Space/Enter — no auto-repeat needed since real keyboard does that), OR document this trade-off if intentional.
|
||||
|
||||
- **GameView.svelte:46 — tile-size margin estimate is fragile.**
|
||||
`margin = 220` (coarse) assumes dock takes ~120px (52px buttons + 12px padding + 12px safe-area + 8px gap × 2 + …). Actual rendered dock height with three 44px action buttons stacked + 8px gap = `44*3 + 8*2 = 148px` plus padding-bottom `12 + safe-area-bottom`. On iPhone with 34px home-indicator inset, dock height ≈ 148 + 12 + 34 = 194px. Add hud (~80) + #app padding (~48) → margin should be closer to 320 not 220.
|
||||
Net effect: `computeTileSize` over-estimates available height, so `maxByHeight` may exceed real space → board overflows and `.board-wrap`'s internal `overflow: auto` kicks in. Functionally OK (board scrolls inside its wrapper), but defeats the "fit the puzzle on screen" goal.
|
||||
Cheap fix: remeasure on a real iPhone and adjust margin to ~260–280; or use `getBoundingClientRect()` on the dock once mounted and feed back into the calc.
|
||||
|
||||
- **GameView.svelte:208–213 — `min-height: 100dvh` not gated on coarse pointer.**
|
||||
Desktop users get the same column stretch even though desktop hides MobileControls. Result: lots of empty space below the board on a tall desktop window. Cosmetic but worth scoping the rule to `@media (pointer: coarse)` when fixing the High item above.
|
||||
|
||||
## Low
|
||||
|
||||
- **MobileControls.svelte:38, 30 — `disarm()` writes `armTimer = null` but `handleReset` (line 43) overwrites with `setTimeout(...)` after `clearTimeout(armTimer)`.**
|
||||
Race-free, but `disarm` setting `armTimer = null` is pointless — the `clearTimeout(null)` is a no-op anyway. Drop `armTimer = null` from `disarm` for symmetry, or keep it and document why. Nit.
|
||||
|
||||
- **MobileControls.svelte:11–24 — `holdTimer` is plain `let` (not `$state`), correctly chosen** (no template binding needs reactivity). Good, but worth a one-line comment so future readers don't "fix" it to `$state`.
|
||||
|
||||
- **MobileControls.svelte:62 — `aria-label={resetArmed ? 'Tap again to confirm reset' : 'Reset'}` changes mid-interaction.**
|
||||
Screen readers may not re-announce. Consider `aria-live="polite"` on a sibling `<span class="visually-hidden">` instead of toggling label, so the state change is announced. Low priority since the visible label also changes ("RESET" → "TAP AGAIN").
|
||||
|
||||
- **GameView.svelte:67–71 — `won` triggers but interval keeps firing until pointer release.**
|
||||
Not a real bug (`tryMove` early-returns on `won`), but timer runs idle for up to a few seconds between win and finger lift. Cosmetic: `endHold()` on win, or check `won` in the interval body. Skip unless profiling shows it matters.
|
||||
|
||||
## Nit
|
||||
|
||||
- **App.svelte:34** — extracting the footer-visible logic into a named local (`const showFooter = view !== 'game'`) would read slightly cleaner if the condition grows. Trivial.
|
||||
- **GameView.svelte:47** — comment "in-flow mobile dock" is helpful; consider also noting that the 220 figure was eyeballed and tracking issue if measured-vs-actual diverges (ties to Medium item above).
|
||||
- **MobileControls.svelte:72** — repeated inline arrow handlers across four buttons is fine but a small `makeStartHold(dx, dy)` factory would DRY. Optional.
|
||||
- **App.svelte:51** — `pointer-events: none` on footer with `pointer-events: auto` on the link is a nice touch — preserves the original mistouch-mitigation idea even where the footer is shown. No change needed.
|
||||
|
||||
## Edge Cases Scouted
|
||||
|
||||
- Pointer capture on iOS: confirmed `pointerleave` won't fire on touch (implicit pointer capture), but `pointerup` always does — auto-repeat correctly ends on release. No action needed.
|
||||
- Win mid-hold: GameView gates `tryMove` on `won`; interval fires harmlessly until release. Not a regression.
|
||||
- Component unmount mid-hold (back nav): `$effect(() => () => {...})` cleanup runs on Svelte 5 unmount per docs. Verified — this works even with no reactive deps because cleanup tied to effect lifecycle, not deps. Sources: [svelte.dev/$effect docs](https://svelte.dev/docs/svelte/$effect), [Svelte tutorial](https://svelte.dev/tutorial/svelte/effects).
|
||||
- Two-finger flick (slide from one arrow to another): on touch, due to implicit pointer capture, the pointer stays bound to the first button until release — sliding off doesn't re-trigger pointerdown on the new button. Expected and consistent with hardware D-pad.
|
||||
- Reset arming persists across UNDO/LVLS taps: confirmed acceptable per plan. Only the 2s timeout disarms it. Note: tapping LVLS while armed will navigate away and unmount the component — `$effect` cleanup clears `armTimer` properly. No leak.
|
||||
- `e.preventDefault()` on `pointerdown`: blocks synthesized click and focus, both desirable. No `onclick` on arrows so no double-fire risk.
|
||||
- Safe-area: `viewport-fit=cover` set in index.html → `env()` resolves correctly. App.svelte footer and MobileControls dock both honor insets.
|
||||
- `class:armed={resetArmed}` correctly compiles in Svelte 5; no migration concern.
|
||||
|
||||
## Positive Observations
|
||||
|
||||
- `$effect` cleanup pattern is correct for Svelte 5 — no `onDestroy` import needed.
|
||||
- `pointer: coarse` media query gates the entire dock — desktop unaffected.
|
||||
- Pointer Events (vs touch+mouse separately) is the right choice; covers hybrid devices.
|
||||
- In-flow flex layout (decision α) eliminates the z-index / overlap class entirely — much better than padding-fudge alternative.
|
||||
- Two-tap-arming reuses standard CSS variables — no new theme tokens needed (modulo the contrast issue).
|
||||
- `pointer-events: none` on footer with auto on the link (App.svelte:51,59) — already a defensive layer for the brief moment between view transitions.
|
||||
- Brainstorm decisions all honored, including the "last wins" semantic for arrows.
|
||||
|
||||
## Recommended Actions (priority order)
|
||||
|
||||
1. Fix `.game` min-height interaction with `#app` padding (High #1) — gate to coarse pointer or use `100%`.
|
||||
2. Address armed-button contrast with stronger danger token or white-on-darker-red (High #2).
|
||||
3. Re-measure and adjust `computeTileSize` margin to ~260–280 OR refactor to measure dock height (Medium #4).
|
||||
4. Decide and document multi-touch behavior in source (Medium #1).
|
||||
5. Optionally restore keyboard activation on arrow buttons (Medium #2).
|
||||
|
||||
## Metrics
|
||||
|
||||
- LOC net: +~110 / −~30
|
||||
- Files changed: 3
|
||||
- Build: not run (review only — recommend `pnpm build` smoke-test before commit)
|
||||
- Lint: not run
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
- Is the multi-touch "release-of-finger-2-cancels-finger-1" semantic intentional, or just an implementation artifact? Brainstorm doc says "last wins" for press but doesn't address release.
|
||||
- Has the dock height been measured on a real iPhone with 34px home-indicator inset? The 220 margin in `computeTileSize` smells low.
|
||||
- Is there an a11y target (AA / AA-large / no target) for this project? Affects whether the contrast finding is blocking.
|
||||
|
||||
**Status:** DONE_WITH_CONCERNS
|
||||
**Summary:** Implementation is sound and honors plan; two real issues (page-scroll bug from `min-height: 100dvh`, WCAG-AA fail on armed RESET) should land before commit.
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: Mobile Mistouch & Layout Fix — Brainstorm
|
||||
date: 2026-04-28
|
||||
slug: mobile-mistouch-and-layout
|
||||
related:
|
||||
- reports/ui-ux-260428-0935-mobile-footer-mistouch.md
|
||||
status: design-approved
|
||||
---
|
||||
|
||||
# Mobile Mistouch & Layout Fix
|
||||
|
||||
## Problem
|
||||
|
||||
Mobile users report:
|
||||
1. Mistouches on `miti99.com` footer link during gameplay.
|
||||
2. (Surfaced in this session) D-pad / action stack overlap the board view on small viewports.
|
||||
|
||||
Footer review report (`ui-ux-260428-0935-mobile-footer-mistouch.md`) covers root cause: fixed footer link sits in same band as D-pad with sub-WCAG hit area.
|
||||
|
||||
## Decisions (user-approved)
|
||||
|
||||
| # | Decision |
|
||||
|---|----------|
|
||||
| D1 | Footer: hide on game view only. Visible on Menu + Level Select. |
|
||||
| D2 | Add `env(safe-area-inset-bottom)` to footer offset on screens where it remains. |
|
||||
| D3 | Reset button gains confirm step (no more one-tap destructive wipe). |
|
||||
| D4 | D-pad gains press-and-hold repeat (parity with keyboard's REPEAT_MS=130). |
|
||||
| D5 | D-pad/action stack must not overlap the board view. |
|
||||
| D6 | Create implementation plan first (not direct implement). |
|
||||
|
||||
## Layout root cause (D5)
|
||||
|
||||
`MobileControls.svelte` uses `position: fixed; z-index: 50` for both dock-left and D-pad.
|
||||
`GameView.svelte` `.board-wrap` reserves vertical space via `max-height: calc(100vh - 260px)` on `(pointer: coarse)` — but `overflow: auto` means tall puzzles scroll **under** the fixed D-pad. Width is unconstrained — board content can sit horizontally beneath the corner-pinned dock.
|
||||
|
||||
`computeTileSize` margin=260 sizes tiles to fit, but if viewport is small enough or aspect ratio awkward, scroll regions appear and the floating dock visually covers them.
|
||||
|
||||
## Approach options for D5 (layout)
|
||||
|
||||
### α — In-flow controls (recommended)
|
||||
Convert MobileControls from `position: fixed` into a normal grid/flex row at the bottom of `.screen.game`. Board-wrap becomes the flex-grow child, dock occupies its own row.
|
||||
|
||||
- **Pros:** zero overlap by construction. Browser handles z-index, scroll, safe-area naturally. Future-proof.
|
||||
- **Cons:** more layout refactor (GameView CSS changes). Need to ensure dock-left + D-pad row fits on one line on narrow phones (~360px) — likely fine since dock-left is column-stacked, D-pad is the wider element.
|
||||
|
||||
### β — Fixed + page padding
|
||||
Keep fixed positioning, add `padding-bottom: calc(260px + safe-area)` to `.screen.game` and remove `max-height` clamp from board-wrap. Scroll body, not board.
|
||||
|
||||
- **Pros:** smaller diff. No structural change.
|
||||
- **Cons:** still uses fixed dock — page scrolling on iOS tends to fight gesture-blocking. Tile size calc still needs the 260 reserve. Doesn't fix the conceptual smell.
|
||||
|
||||
### γ — Hybrid: fixed dock + spacer div
|
||||
Keep dock fixed, insert a flex spacer with matching height inside `.screen.game` to push board upward. Equivalent visually to α with less refactor risk.
|
||||
|
||||
- **Pros:** middle ground. Dock stays in its corner.
|
||||
- **Cons:** spacer is dead markup. Two sources of truth for dock height.
|
||||
|
||||
**Pick α.** KISS+YAGNI: stop fighting fixed positioning, put the dock where it belongs — in the document flow.
|
||||
|
||||
## Approach options for D3 (Reset confirm)
|
||||
|
||||
### a — Two-tap arming (recommended for mobile)
|
||||
First Reset tap arms the button (label changes "RESET" → "TAP AGAIN"), 2-second timeout to disarm. Second tap actually resets.
|
||||
|
||||
- **Pros:** zero modal infra, no z-index war. One file change. Short interaction.
|
||||
- **Cons:** discoverability (user must learn the pattern). Mitigated by visible label change.
|
||||
|
||||
### b — Confirm modal
|
||||
Reuse existing dialog component. Yes/Cancel.
|
||||
|
||||
- **Pros:** explicit, no learning curve.
|
||||
- **Cons:** heavier interaction, modal infra already used by win + donate. Overkill for "are you sure?"
|
||||
|
||||
### c — Long-press to reset
|
||||
Tap = no-op; long-press (500ms) = reset.
|
||||
|
||||
- **Pros:** native gesture, no UI change.
|
||||
- **Cons:** undiscoverable. Conflicts with mobile gesture-blocking. Reject.
|
||||
|
||||
**Pick a.** Two-tap arming.
|
||||
|
||||
## Approach options for D4 (D-pad hold-repeat)
|
||||
|
||||
### i — pointerdown + setInterval (recommended)
|
||||
On `pointerdown`, fire move immediately, start `setInterval(REPEAT_MS=130)` until `pointerup` / `pointercancel` / `pointerleave`. Uses Pointer Events for unified mouse+touch.
|
||||
|
||||
### ii — touchstart-only
|
||||
Same logic but touch-only events. Misses mouse users on hybrid devices (Surface, iPad+trackpad).
|
||||
|
||||
**Pick i.** Pointer Events covers all coarse-pointer cases.
|
||||
|
||||
## Approach for D1+D2 (footer)
|
||||
|
||||
Single conditional in `App.svelte`:
|
||||
|
||||
```svelte
|
||||
{#if view !== 'game'}
|
||||
<footer class="site-footer">…</footer>
|
||||
{/if}
|
||||
```
|
||||
|
||||
Add `bottom: calc(6px + env(safe-area-inset-bottom));` to `.site-footer` for menu/levels screens.
|
||||
|
||||
## Files affected
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/App.svelte` | conditional footer + safe-area inset |
|
||||
| `src/views/GameView.svelte` | layout refactor: board-wrap grows, dock occupies bottom row; update `computeTileSize` margin if needed |
|
||||
| `src/views/MobileControls.svelte` | drop `position: fixed`; add hold-repeat to arrow buttons |
|
||||
| `src/views/AppButton.svelte` or new mini state | possibly armed-state styling for Reset (or inline in MobileControls) |
|
||||
|
||||
## Risks
|
||||
|
||||
- Layout refactor may regress portrait/landscape edge cases on iPad. Mitigation: manual smoke test 360×640, 414×896, 768×1024.
|
||||
- Hold-repeat with box-pushing could feel violent if too fast. Mitigation: reuse REPEAT_MS=130, same as keyboard.
|
||||
- Reset arming-state UX needs clear visual delta. Mitigation: color/icon swap, not just text.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Cannot tap miti99 link during gameplay.
|
||||
- D-pad never visually overlaps board content at common phone sizes (360–430 wide).
|
||||
- Reset requires two taps within 2s window.
|
||||
- Holding a D-pad arrow continuously moves the player at keyboard parity.
|
||||
- Footer respects safe-area-inset on iPhone X+ home-indicator.
|
||||
- Build passes; no console errors on touch devices.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Sound effects (Phase 4).
|
||||
- Player facing direction (Phase 4).
|
||||
- Level category tabs (Phase 4).
|
||||
|
||||
## Open questions
|
||||
|
||||
(none — all clarifications resolved during AskUserQuestion round)
|
||||
|
||||
## Next step
|
||||
|
||||
Invoke `/ck:plan` with this report as context to produce phased implementation plan in `plans/260428-1004-mobile-mistouch-fix/`.
|
||||
@@ -0,0 +1,171 @@
|
||||
# Mobile UX Review — Footer Mistouch on `miti99.com` Link
|
||||
|
||||
**Date:** 2026-04-28
|
||||
**Scope:** Sokoban Svelte 5 — mobile gameplay footer mistouch bug
|
||||
**Files reviewed:** `src/App.svelte`, `src/views/MobileControls.svelte`, `src/views/GameView.svelte`, `src/app.css`, `index.html`
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
Footer link sits exactly on top of the mobile D-pad / action stack on small screens. Footer is `position: fixed; bottom: 6px` while D-pad and action stack are `bottom: calc(12px + safe-area-inset-bottom)`. Both render in the same vertical band (~6px–112px from viewport bottom). Even though footer wrapper has `pointer-events: none`, the `<a>` re-enables `pointer-events: auto` and sits above nothing — but importantly, it sits in the gap **between** the dock-left action stack and the D-pad (centered text), where users' thumbs swipe between Undo/Reset and the arrows. Mistouches happen on the centered link text directly.
|
||||
|
||||
Also: footer ignores `safe-area-inset-bottom`, so on iPhones with home indicator the link can collide with the system gesture bar too.
|
||||
|
||||
**Recommendation:** Hide footer on `view === 'game'` (Option A — KISS, ships in 2 lines). All other options are layered nice-to-haves.
|
||||
|
||||
---
|
||||
|
||||
## Diagnosis
|
||||
|
||||
### 1. Geometric overlap (the bug)
|
||||
|
||||
Measurements at default 16px root font:
|
||||
|
||||
| Element | Bottom anchor | Approx height | Top edge from viewport bottom |
|
||||
|---|---|---|---|
|
||||
| Footer text | `bottom: 6px` | ~14px (12px font + line) | ~6px–20px |
|
||||
| Footer `<a>` (`miti99`) | centered horizontally | ~14px tall, ~50px wide | same band as above |
|
||||
| D-pad | `bottom: 12px + safe-area` | 116px (2×56 + 4 gap) | ~12px–128px (right side) |
|
||||
| Action stack | `bottom: 12px + safe-area` | 148px (3×44 + 2×8 gap) | ~12px–160px (left side) |
|
||||
|
||||
The footer link sits in the **horizontal center**, between the action stack (left) and D-pad (right). On phones 360–414px wide:
|
||||
- Action stack right edge: ~12 + 64 = **76px from left**
|
||||
- D-pad left edge: viewport_width − 12 − 172 = **viewport_width − 184px from left**
|
||||
- Center gap on a 390px iPhone 13: **76px to 206px** — that's a 130px wide horizontal corridor where the footer link lives.
|
||||
|
||||
The link is centered around viewport mid (~195px on 390px wide), and reaches into the **vertical** band of both controls (6–20px from bottom). The action stack and D-pad both extend **down to 12px from bottom**, so the footer is just 6px above the bottoms of both controls.
|
||||
|
||||
**Mistouch scenario:** User's thumb is repeatedly tapping bottom-row D-pad arrows (Down/Left/Right at `bottom: 12px`). A slightly off thumb that lands on the link instead — or a swipe from D-pad bottom edge towards the action stack — hits the footer link and yanks them off to `miti99.com` in a new tab.
|
||||
|
||||
### 2. Target size violation (WCAG 2.5.5 AA / 2.5.8 AAA)
|
||||
|
||||
Footer link `<a>` has:
|
||||
- `font-size: 12px`
|
||||
- No `padding`, no `min-height`, no `min-width`
|
||||
- Effective hit area: ~50×14px
|
||||
|
||||
WCAG 2.5.5 (Level AAA) and 2.5.8 (Level AA, WCAG 2.2): minimum **24×24 CSS px** target size, recommended **44×44** (Apple HIG) / **48×48dp** (Material). This link is **~4× too short** vertically.
|
||||
|
||||
A 14px-tall target near the dominant thumb zone is a finger trap.
|
||||
|
||||
### 3. Footer purpose vs. context
|
||||
|
||||
Footer is decorative attribution — `Made with ♥ by miti99`. It is global (rendered in `App.svelte` outside the view router), so it appears on:
|
||||
- Menu (desired — branding)
|
||||
- Levels (acceptable — branding)
|
||||
- Game (problematic — overlaps controls, no informational value during play)
|
||||
|
||||
YAGNI: footer adds zero value during active gameplay. Author credit belongs on menu/about screens.
|
||||
|
||||
### 4. Safe-area neglect
|
||||
|
||||
`bottom: 6px` does not include `env(safe-area-inset-bottom)`. On iPhone X+ portrait the home-indicator gesture bar lives in the bottom ~34px, so the footer text is **drawn underneath the gesture bar** — invisible or partially clipped, and any tap there fights the iOS system gesture (swipe-up to home). Even worse, the link is rendered into a region the user is conditioned to swipe through.
|
||||
|
||||
### 5. Other mobile UX issues spotted
|
||||
|
||||
- **`MobileControls` action buttons (`UNDO / RESET / LVLS`) are `min-width: 64px; height: 44px`** — meet 44×44 minimum but are stacked at `gap: 8px`. Reset is between Undo and Lvls. A mis-tap on Reset wipes progress with no confirmation. Suggest adding a tap-to-confirm or a 2s "tap again to confirm reset" pattern.
|
||||
- **D-pad missing center button** — grid has empty `down` cell only when laid out as defined. Looking again: the grid is `". up ." / "left down right"` — that's fine, no overlapping cells. But arrow buttons have **no explicit `width`/`height`** other than the grid cell (56×56). Visual size matches WCAG; OK.
|
||||
- **No auto-repeat on D-pad** — comment says "Tap-only — no auto-repeat." Long pushes through corridors require many discrete taps. Consider press-and-hold repeat at REPEAT_MS=130 (already used for keyboard) for parity. Optional, not the bug.
|
||||
- **Board overflow uses `overflow: auto`** which on mobile creates a scrollable area inside the viewport. Combined with `touch-action: manipulation` on buttons but not on board, two-finger zoom and pan inside board are possible — could be intentional for very large mazes.
|
||||
- **Header `desktop-actions` already hidden on coarse pointer** — good. But level name and stats wrap at small width; level name is 22px and could be 18px on phones to save vertical room.
|
||||
- **Win overlay buttons** — fine, modal centered, large hit areas.
|
||||
- **`<svelte:window onkeydown={onKey}>`** — fine for keyboard. No mobile-specific concern.
|
||||
- **`-webkit-tap-highlight-color: transparent` globally** — removes the blue flash, which is good for game feel but reduces perceived feedback on the footer link. Combined with no `:active` style on the link, taps feel like nothing happened — until a new tab pops open. Reinforces the "wait, I didn't mean to" reaction.
|
||||
|
||||
---
|
||||
|
||||
## Fix Proposals (ranked simple → involved)
|
||||
|
||||
### Option A — Hide footer during gameplay (recommended)
|
||||
|
||||
**Change:** Move `<footer>` rendering inside the `{#if view === 'menu' || view === 'levels'}` branches, or add `{#if view !== 'game'}` guard around it in `App.svelte`.
|
||||
|
||||
**Pros:**
|
||||
- 2-line change. KISS / YAGNI.
|
||||
- Eliminates the bug entirely on the screen where it matters.
|
||||
- Branding still shown on menu and level select where players land.
|
||||
- No CSS gymnastics, no z-index battles.
|
||||
|
||||
**Cons:**
|
||||
- Loses attribution on the game screen. Acceptable — attribution belongs on menu.
|
||||
- Doesn't fix the WCAG target-size issue on menu/levels (still 12px, ~14px tall). Low risk there because no overlapping controls, but link is still small.
|
||||
|
||||
**Code sketch:**
|
||||
```svelte
|
||||
{#if view !== 'game'}
|
||||
<footer class="site-footer"> ... </footer>
|
||||
{/if}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option B — Move footer above the controls + enlarge hit area
|
||||
|
||||
**Change:**
|
||||
- Bump footer to `bottom: calc(140px + env(safe-area-inset-bottom))` on coarse pointer (above the D-pad/action stack).
|
||||
- Add `padding: 8px 16px; display: inline-block` to the `<a>` to hit ~30px tall × 80px wide.
|
||||
- Add `:active` state for tap feedback.
|
||||
|
||||
**Pros:**
|
||||
- Keeps attribution visible everywhere.
|
||||
- Improves WCAG compliance.
|
||||
|
||||
**Cons:**
|
||||
- Footer floating mid-screen above controls looks awkward and steals visual focus from the puzzle.
|
||||
- Still risks overlap with the win overlay close zone.
|
||||
- More CSS than Option A and doesn't reduce surface area for mistouches as much as removing it does.
|
||||
|
||||
---
|
||||
|
||||
### Option C — Footer on menu/levels only, plus credit line in game HUD
|
||||
|
||||
**Change:**
|
||||
- Apply Option A (hide footer in game view).
|
||||
- Add a small `Made by miti99` line inside the win modal (or in the menu's existing credit area).
|
||||
|
||||
**Pros:**
|
||||
- Branded everywhere it makes sense, never in the way during play.
|
||||
- Win modal is a natural moment for attribution + donate CTA (donate modal already there).
|
||||
|
||||
**Cons:**
|
||||
- Slight extra work (~10 lines).
|
||||
- Two places to maintain credit.
|
||||
|
||||
---
|
||||
|
||||
### Option D — Keep footer, make it non-interactive during gameplay
|
||||
|
||||
**Change:** When `view === 'game'`, render footer with `pointer-events: none` on **both** wrapper and `<a>` (override the `pointer-events: auto`). Visually visible, totally untappable.
|
||||
|
||||
**Pros:**
|
||||
- Branding stays visible. Zero mistouches because nothing is clickable.
|
||||
|
||||
**Cons:**
|
||||
- Defeats the purpose of having a link. Why show a link nobody can tap?
|
||||
- Footer text still overlaps gesture bar / safe area on iPhone — visual clutter unaddressed.
|
||||
- Worst of both worlds: present but useless.
|
||||
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Ship Option A.** It is the smallest, safest fix and addresses the root cause (footer in the gameplay zone serves no purpose). If branding-during-play is desired later, layer Option C on top.
|
||||
|
||||
Independently, two follow-ups regardless of choice:
|
||||
1. Add `bottom: calc(6px + env(safe-area-inset-bottom))` to `.site-footer` so the menu/levels footer respects the iPhone home indicator.
|
||||
2. Increase footer link tap padding (`padding: 6px 10px`) so the menu/levels link meets 24×24 minimum target size.
|
||||
|
||||
---
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
1. Is `miti99.com` link a hard requirement on every screen (sponsorship/license/contractual)? If yes, Option B or C; if not, Option A.
|
||||
2. Do telemetry / referrer logs show how many `miti99.com` clicks come from mobile vs. desktop? High mobile rate would confirm the mistouch theory empirically.
|
||||
3. Should Reset action gain a confirm-tap given it is adjacent to Undo? Out of scope for this bug but flagged as a related risk.
|
||||
4. Press-and-hold auto-repeat on D-pad — desired, or intentionally tap-only for puzzle thinking time?
|
||||
|
||||
---
|
||||
|
||||
**Status:** DONE
|
||||
**Summary:** Diagnosed mistouch as overlap of fixed footer link with mobile D-pad/action-stack tap zones plus WCAG target-size violation; recommended Option A (hide footer on game view) as KISS fix, with Options B/C/D listed for tradeoff comparison.
|
||||
Reference in new issue
Block a user