mirror of
https://github.com/tiennm99/sokoban.git
synced 2026-10-20 00:15:45 +00:00
feat(mobile): portrait upward bias + landscape side-by-side layout
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.
This commit is contained in:
1 parent
e08beb0c81
commit
0fa7a58521
2 files changed
+83
-12
No files matched your search
+60
-12
@@ -41,13 +41,29 @@
|
||||
if (!level) return 48;
|
||||
const maxTile = 56;
|
||||
const minTile = 16;
|
||||
// Dock is now in-flow (flex row at bottom); margin only covers header + hud + padding.
|
||||
const isCoarse = typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches;
|
||||
// 195 reflects the slimmer mobile chrome after R6+R7 (top-aligned d-pad
|
||||
// at 48px arrows). If the dock layout changes, retune this.
|
||||
const margin = isCoarse ? 195 : 120; // header + hud (+ in-flow mobile dock) + padding
|
||||
const maxByWidth = Math.floor((window.innerWidth - 80) / level.width);
|
||||
const maxByHeight = Math.floor((window.innerHeight - margin - 100) / level.height);
|
||||
const hasMM = typeof window !== 'undefined';
|
||||
const isCoarse = hasMM && window.matchMedia('(pointer: coarse)').matches;
|
||||
const isLandscape = hasMM && window.matchMedia('(orientation: landscape)').matches;
|
||||
|
||||
// Chrome reserve depends on layout:
|
||||
// coarse + landscape: dock lives in a side column, so vertical chrome
|
||||
// is just HUD + paddings (no in-flow dock below the board).
|
||||
// coarse + portrait : dock stacks under the board.
|
||||
// desktop : HUD only.
|
||||
let chromeY, chromeX;
|
||||
if (isCoarse && isLandscape) {
|
||||
chromeY = 76;
|
||||
chromeX = 80 + 160; // base + side dock width
|
||||
} else if (isCoarse) {
|
||||
chromeY = 295;
|
||||
chromeX = 80;
|
||||
} else {
|
||||
chromeY = 220;
|
||||
chromeX = 80;
|
||||
}
|
||||
|
||||
const maxByWidth = Math.floor((window.innerWidth - chromeX) / level.width);
|
||||
const maxByHeight = Math.floor((window.innerHeight - chromeY) / level.height);
|
||||
return Math.max(minTile, Math.min(maxTile, maxByWidth, maxByHeight));
|
||||
}
|
||||
|
||||
@@ -127,14 +143,20 @@
|
||||
tileSize = computeTileSize();
|
||||
}
|
||||
|
||||
// Recompute tile size when pointer type changes (e.g. external keyboard
|
||||
// attached/detached on iPad), so the bottom-controls reservation matches.
|
||||
// Recompute tile size when pointer type or orientation changes — the
|
||||
// chrome reserve differs between portrait (dock stacks below board) and
|
||||
// landscape (dock moves to a side column), so the tile budget shifts.
|
||||
$effect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const mq = window.matchMedia('(pointer: coarse)');
|
||||
const mqCoarse = window.matchMedia('(pointer: coarse)');
|
||||
const mqOrient = window.matchMedia('(orientation: landscape)');
|
||||
const handler = () => onResize();
|
||||
mq.addEventListener('change', handler);
|
||||
return () => mq.removeEventListener('change', handler);
|
||||
mqCoarse.addEventListener('change', handler);
|
||||
mqOrient.addEventListener('change', handler);
|
||||
return () => {
|
||||
mqCoarse.removeEventListener('change', handler);
|
||||
mqOrient.removeEventListener('change', handler);
|
||||
};
|
||||
});
|
||||
|
||||
const hasNext = $derived(levelIndex + 1 < MICROBAN_LEVELS.length);
|
||||
@@ -280,8 +302,12 @@
|
||||
|
||||
.game { gap: 10px; }
|
||||
|
||||
/* Asymmetric padding-bottom shifts the safe-centered group upward by
|
||||
roughly half the padding (~28px on a 915px viewport). Counters the
|
||||
perceptual "low" feel caused by HUD top-weight + dock visual mass. */
|
||||
.play-stack {
|
||||
justify-content: safe center;
|
||||
padding-bottom: clamp(16px, 6vh, 56px);
|
||||
}
|
||||
|
||||
.play-group {
|
||||
@@ -298,6 +324,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Landscape phones: flip .play-group to a row so the dock occupies a
|
||||
narrow right column instead of stacking below the board. Lets the board
|
||||
claim the full vertical viewport (minus HUD), 3x the tile size on most
|
||||
Microban levels. Drop the portrait upward-bias padding — the row layout
|
||||
fills space differently. */
|
||||
@media (pointer: coarse) and (orientation: landscape) {
|
||||
.play-stack { padding-bottom: 0; }
|
||||
|
||||
.play-group {
|
||||
flex: 1 1 auto;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.board-wrap {
|
||||
flex: 1 1 auto;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--danger);
|
||||
font-weight: 700;
|
||||
|
||||
@@ -158,6 +158,29 @@
|
||||
.action { height: 48px; }
|
||||
}
|
||||
|
||||
/* Landscape phones: dock becomes a narrow right column. Action stack
|
||||
sits at the top, d-pad at the bottom so the arrows land in the
|
||||
natural thumb arc for a two-handed grip. */
|
||||
@media (pointer: coarse) and (orientation: landscape) {
|
||||
.mobile-dock {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 160px;
|
||||
gap: 12px;
|
||||
padding:
|
||||
12px
|
||||
calc(8px + env(safe-area-inset-right))
|
||||
calc(12px + env(safe-area-inset-bottom))
|
||||
8px;
|
||||
}
|
||||
|
||||
/* Override portrait's align-self: flex-start; in column layout the
|
||||
cross axis is horizontal and we want the d-pad centered. */
|
||||
.dpad { align-self: center; }
|
||||
.dock-left { align-self: center; }
|
||||
}
|
||||
|
||||
/* Global `button` rules (touch-action, tap-highlight) live in app.css. */
|
||||
.action {
|
||||
min-width: 64px;
|
||||
|
||||
Reference in new issue
Block a user