Bonus layouts live in their own module so the license boundary stays
clean: Microban levels remain under Skinner's redistribution grant,
while the hand-designed ones fall under the repository's main LICENSE.
A new levels module concatenates both sets into one index space, so
progress-store keys and level numbering continue unbroken. Level select
tags bonus entries so they read as distinct from the stock set.
Replace pnpm-lock.yaml with package-lock.json. The security overrides move to
package.json#overrides with their range operators intact, so advisory floors
stay floors. allowBuilds becomes package.json#allowScripts where the listed
package is actually in the tree.
The level-select pager is the last child of .screen and lands near the
viewport bottom on shorter screens, where the position: fixed
.site-footer ("made by miti99") was overlapping it. Add a
margin-bottom of calc(48px + safe-area-inset-bottom) so the pager
always sits above the footer's hit area.
Tighten everything that fights the board for pixels in landscape:
- #app top padding 12 -> 6 (saves 6px vertical).
- .game gap 10 -> 6 (saves 4px between HUD and board).
- D-pad arrows 48 -> 44px and action button height 48 -> 44px (still
above the 44pt HIG minimum). Dock width shrinks 160 -> 150px and
internal d-pad height 102 -> 94px.
- Dock now justify-content: flex-end so the action stack and d-pad
cluster at the bottom of the column (natural thumb arc), leaving
empty space above instead of mid-column.
computeTileSize landscape branch: chromeY 76 -> 64, chromeX
240 -> 186 to match the slimmer chrome reservation. Net board win on
800x360 phones: ~47 -> 50 tile (+6%); on Pixel 7 landscape Microban 1
already hits the 56px cap and stays there. Tall puzzles still bound
by viewport height — no CSS-only path past that without scroll.
All changes gated behind @media (pointer: coarse) and (orientation:
landscape). Portrait and desktop untouched.
Portrait: add asymmetric padding-bottom: clamp(16px, 6vh, 56px) on
.play-stack so safe-center distributes the slack into the centroid,
lifting the play-group ~28px upward. Counters the perceptual "low"
feel caused by HUD top-weight + dock visual mass even though the
group is geometrically near-centered.
Landscape: flip .play-group to flex-direction: row so the dock
occupies a 160px right column instead of stacking below the board.
.mobile-dock switches to flex-direction: column with
justify-content: space-between (action stack on top, d-pad at bottom
for the natural two-handed thumb arc). On a Pixel 7 landscape
viewport this lifts Microban 1's tile size from 19px to 56px (3x
bigger) and brings the giant finale levels back into a playable
range.
computeTileSize now branches on orientation: landscape uses
chromeY=76 (HUD only) and reserves 160px of width for the side dock;
portrait keeps chromeY=295 (HUD + in-flow dock); desktop unchanged.
Orientation matchMedia listener wired into the existing pointer
listener so rotation reflows the tile budget live.
Touch targets unchanged (48px arrows, 48px action buttons). All
recent fixes (mistouch, top-aligned d-pad, LVLS grid, play-group
center) preserved — only the OUTER flex direction flips on
landscape; internal MobileControls grids are unaffected.
Group [board + 8px gap + MobileControls] inside a single .play-group
div and vertically center it in the space below the HUD on touch
devices. The dock no longer hangs at the bottom edge of the viewport;
instead the whole [board + controls] reads as one unit floating in the
middle of the screen.
Implementation: a new .play-stack passthrough flex container fills the
remaining column space; on coarse pointers it uses
justify-content: safe center to center .play-group, which itself goes
flex: 0 1 auto with max-height: 100%. The "safe" keyword falls through
to flex-start when the group overflows, so tall finale puzzles still
scroll inside .board-wrap instead of clipping above the HUD.
Replaces the prior coarse-only .board-wrap { align-items: flex-end }
strategy and the now-redundant .board { margin-top: auto } belt-and-
suspenders rule in Board.svelte. Desktop layout unchanged.
Restructure .dock-left from a 3-button column-flex into a 2x2 grid:
UNDO and RESET share the top row, LVLS spans the full bottom row via
grid-column: 1 / -1. Both columns now share 48px row heights and a 6px
gap so UNDO/RESET line up with UP and LVLS lines up with the
LEFT/DOWN/RIGHT row.
Bump .action height 44 -> 48px on coarse pointers so action rows match
the d-pad's 48px arrows. Still well above the 44pt HIG minimum. With
the action stack now equal to the d-pad's natural height (102px), the
dock no longer needs the previous round's asymmetric flex-end layout
to look balanced.
Lifts the d-pad and action stack further off the screen edge on
Android (where safe-area-inset-bottom is 0). iOS devices still
compound the inset on top so home-indicator clearance is unchanged.
Round 1 anchored the board to .board-wrap's bottom but exposed a 32px
gap caused by .mobile-dock { align-items: flex-end } combined with
.dock-left (148px tall) being taller than .dpad (116px). With both
columns bottom-aligned, the d-pad's top sat ~32px below the action
stack's top, so the arrows still felt far from the board.
Switch the dock to align-items: stretch so each column picks its own
cross-axis position: action stack stays bottom-anchored (closer to the
thumb), d-pad uses align-self: flex-start to hug the dock top (closer
to the board). Movement is the primary interaction; UNDO/RESET are
secondary. Shrink arrows 56 -> 48px (still well above 44pt HIG) and
bump grid gap 4 -> 6px.
Trim the JS computeTileSize coarse-pointer chrome margin 220 -> 195 to
reflect the slimmer dock so the board can grow into the freed space on
tall puzzles. Add an explicit margin-top: auto on .board inside a
coarse media query in Board.svelte as a belt-and-suspenders cross-axis
push that survives future parent-flex refactors.
Combined with the round-1 changes, the board-bottom to arrow-top
distance drops from ~42px to ~10px on touch viewports. Desktop is
untouched (all changes gated by @media (pointer: coarse)).
Short puzzles (e.g. Microban 1) showed ~300px of invisible dead space
between the board and the on-screen D-pad because <Board> is fixed-pixel
and rendered top-left inside a flex-grown wrapper. Anchor .board-wrap
contents to flex-end on coarse pointers so the gap collapses against
the dock instead of below the board. Also drop the redundant 24px #app
bottom padding (the dock already pads safe-area-inset-bottom) and
tighten the .game flex gap from 16px to 10px on coarse pointers.
All changes gated behind @media (pointer: coarse). Desktop layout is
untouched.
Previously hid the footer on the game view to avoid overlap with the
on-screen D-pad on mobile. Render it on every view now and only suppress
it via @media (pointer: coarse) when in-game, so desktop always shows
the credit line while touch devices keep the mistouch-safe behavior.
Footer link was overlapping the on-screen D-pad and action stack tap
zones during gameplay. Hide the footer on the game view and respect
safe-area-inset on the screens where it stays.
MobileControls drops position:fixed and lives in a flex row at the
bottom of GameView's flex column. .board-wrap is now flex:1 1 auto
with min-height:0 so puzzles scroll inside their wrapper, never under
the controls. Magic max-height:calc(100vh - 260px) gone.
RESET on mobile is now two-tap armed (label flips to TAP AGAIN, red
background, 2s timeout) so a stray thumb cannot wipe a puzzle. The R
keyboard shortcut is unchanged.
D-pad arrows fire on pointerdown and auto-repeat at 130ms while held,
matching the keyboard REPEAT_MS. preventDefault on pointerdown blocks
the synthesized click double-fire. $effect cleanup clears intervals
on unmount.
Armed RESET text switched to white (4.85:1 on --danger) to clear
WCAG-AA; using --bg failed at 3.1:1.
Work covered by these plans is already in git history and the changelog:
sokoban-overhaul (2026-04-11), svelte-migration (2026-04-12),
mobile-comfort (2026-04-27), review-fixes-and-dep-prs (2026-04-27).
bg.png (295 KB) and logo.png (24 KB) had zero references in src,
index.html, or any CSS, but matched the workbox precache glob and
shipped to every install. Removing them drops the precache from
555 KB to 235 KB (-58%). Update the docs note.
Also add the review-fixes plan to plans/.
Workbox/vite-plugin-pwa pull in @rollup/plugin-terser, which depends on
older serialize-javascript versions with a known RCE/DoS chain.
Overriding to >=7.0.5 surgically clears the chain without dropping
vite-plugin-pwa to the 0.19.x major. npm audit now reports 0
vulnerabilities; build is unaffected (build-only deps).
- DonateModal: focus the dialog on open so screen readers announce it
and Tab cycles within; restore focus to the previously-active element
on close.
- Move shared overlay/dialog scaffolding to app.css so GameView's win
dialog and DonateModal stop duplicating the same backdrop and frame.
- Move touch-action and tap-highlight neutralization to a global
button rule; drop the per-component copies.
- level-parser: collapse the key/cellKey alias - export cellKey
directly.
- BoardModel.isSolved: inline the empty-box-set guard into the return.
- LevelSelectView: completedCount is read once and never reassigned;
drop the $state wrapper.
- Board: hoist the wall-trim DIRS array out of $derived so it isn't
rebuilt every reactive recomputation.
- README: drop incorrect 'syncs across all devices' claim about
localStorage progress.
- package.json: description still said Phaser 3; it's Svelte 5 now.
- Mark the two old plans as completed (work shipped long ago).
- Add the 4 review reports to plans/reports/.
- GameView: ignore game keys (Esc/R/U/Z) while the donate modal is open
so its own Escape no longer doubles as a navigate-to-levels.
- GameView: tag the win dialog with role/aria-modal/aria-labelledby so
screen readers announce it correctly.
- LevelSelectView: read the progress snapshot once per page render
instead of two localStorage parses per visible tile (40 reads -> 1).
- progress-store: add snapshot() method for batch reads.
- vite-plugin-pwa: include jpg in the workbox precache glob so the
donation QR keeps working offline.
Update codebase summary, system architecture, changelog, roadmap and
PDR to cover the new mobile input layer, haptics, gesture blocking
and PWA. Refresh README features. Add the brainstorm, plan and review
artifacts under plans/.
Register VitePWA in the production config with autoUpdate, Nord theme
colors, /sokoban/ start_url and scope, 192/512 plus a maskable icon.
Workbox precaches the built bundle so subsequent visits run offline.
Generate matching pwa-192/512 and apple-touch-icon PNGs in public/ so
"Add to Home Screen" picks up a proper icon on iOS and Android.
Add on-screen D-pad (bottom-right) and action stack (UNDO / RESET /
LVLS, bottom-left), shown only on coarse-pointer devices. Hide the
duplicated header actions on touch.
Block iOS/Android browser quirks: pull-to-refresh, double-tap zoom,
long-press selection and callout. Game stays inside the viewport via
overscroll-behavior and per-element touch-action rules; viewport zoom
is left enabled for accessibility.
Add navigator.vibrate wrapper (haptics.pulse) and fire short pulses on
box push and longer pulse on level solve. Silent no-op where the API
is missing.
Reserve room for the bottom controls in computeTileSize on coarse
pointer; raise minTile to 16 for legibility on small phones.
Adds npm run dev:codeserver with Vite config tuned for code-server's
/absproxy/<port>/ route: host binding, allowedHosts from env, and HMR
over wss on clientPort 443. Host/port read from .env.local (gitignored);
.env.example documents required vars.