Fix mobile: compress card fan to fit any screen; kill horizontal overflow
- PlayerHand measures viewport and compresses the fan (responsive card size + dynamic overlap) so all 13 cards are visible and tappable on phones — no off-screen cards, no horizontal scroll; added tap feedback - globals: html/body overflow-x hidden + overscroll-behavior none Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,8 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -354,12 +354,28 @@ function TrickArea({
|
||||
|
||||
/* ----------------------------- Player hand ---------------------------- */
|
||||
|
||||
function useViewportWidth() {
|
||||
const [vw, setVw] = useState(typeof window !== "undefined" ? window.innerWidth : 390);
|
||||
useEffect(() => {
|
||||
const f = () => setVw(window.innerWidth);
|
||||
f();
|
||||
window.addEventListener("resize", f);
|
||||
window.addEventListener("orientationchange", f);
|
||||
return () => {
|
||||
window.removeEventListener("resize", f);
|
||||
window.removeEventListener("orientationchange", f);
|
||||
};
|
||||
}, []);
|
||||
return vw;
|
||||
}
|
||||
|
||||
function PlayerHand({ legalIds }: { legalIds: Set<string> }) {
|
||||
const hand = useGameStore((s) => s.game.players[0].hand);
|
||||
const phase = useGameStore((s) => s.game.phase);
|
||||
const turn = useGameStore((s) => s.game.turn);
|
||||
const playHuman = useGameStore((s) => s.playHuman);
|
||||
const { front } = useCardSkins();
|
||||
const vw = useViewportWidth();
|
||||
|
||||
const sorted = sortHand(hand);
|
||||
const myTurn = phase === "playing" && turn === 0;
|
||||
@@ -367,45 +383,53 @@ function PlayerHand({ legalIds }: { legalIds: Set<string> }) {
|
||||
const choosing = phase === "choosing-trump";
|
||||
const n = sorted.length;
|
||||
|
||||
// Compress the fan so every card fits the screen width (no overflow/scroll).
|
||||
const small = vw < 560;
|
||||
const size = vw < 360 ? "sm" : vw < 560 ? "md" : "lg";
|
||||
const cardW = size === "sm" ? 44 : size === "md" ? 60 : 74;
|
||||
const avail = Math.min(vw - 12, 620);
|
||||
const step = n > 1 ? Math.min(cardW * 0.94, Math.max(15, (avail - cardW) / (n - 1))) : 0;
|
||||
const overlap = step - cardW; // negative inline-start margin
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-0 inset-x-0 flex justify-center pb-3 pointer-events-none",
|
||||
"absolute bottom-0 inset-x-0 flex justify-center pb-2 pointer-events-none",
|
||||
choosing ? "z-50" : "z-20"
|
||||
)}
|
||||
>
|
||||
<div className="relative flex items-end justify-center pointer-events-auto">
|
||||
<div className="relative flex items-end justify-center pointer-events-auto max-w-full">
|
||||
{sorted.map((card, i) => {
|
||||
const playable = myTurn && legalIds.has(card.id);
|
||||
const dimmed = myTurn && !playable;
|
||||
const mid = (n - 1) / 2;
|
||||
const rot = (i - mid) * 3.2;
|
||||
const lift = Math.abs(i - mid) * 4;
|
||||
const rot = (i - mid) * (small ? 2 : 3.2);
|
||||
const lift = Math.abs(i - mid) * (small ? 2 : 4);
|
||||
return (
|
||||
<motion.button
|
||||
key={card.id}
|
||||
layout
|
||||
initial={{ y: 120, opacity: 0 }}
|
||||
animate={{ y: lift, opacity: 1, rotate: rot }}
|
||||
transition={{ type: "spring", stiffness: 280, damping: 28, delay: i * 0.015 }}
|
||||
whileHover={playable ? { y: lift - 26, scale: 1.06, zIndex: 50 } : {}}
|
||||
transition={{ type: "spring", stiffness: 280, damping: 28, delay: i * 0.012 }}
|
||||
whileHover={playable ? { y: lift - 26, scale: 1.08, zIndex: 50 } : {}}
|
||||
whileTap={playable ? { scale: 1.05 } : {}}
|
||||
onClick={() => playable && playHuman(card)}
|
||||
disabled={!playable}
|
||||
data-card={card.id}
|
||||
data-playable={playable ? "1" : "0"}
|
||||
style={{ marginInlineStart: i === 0 ? 0 : -22 }}
|
||||
style={{ marginInlineStart: i === 0 ? 0 : overlap }}
|
||||
className={cn(
|
||||
"origin-bottom",
|
||||
playable && "cursor-pointer",
|
||||
!myTurn && "cursor-default"
|
||||
"origin-bottom shrink-0",
|
||||
playable ? "cursor-pointer relative z-30" : "cursor-default"
|
||||
)}
|
||||
>
|
||||
<PlayingCard
|
||||
card={card}
|
||||
size="lg"
|
||||
size={size}
|
||||
dimmed={dimmed}
|
||||
front={front}
|
||||
className={cn(playable && "ring-2 ring-gold-400/70")}
|
||||
className={cn(playable && "ring-2 ring-gold-400/80")}
|
||||
/>
|
||||
</motion.button>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user