From 0b376aec1635bb5907e8f72c5f2f871cace93ce9 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 4 Jun 2026 16:00:00 +0330 Subject: [PATCH] Fix mobile: compress card fan to fit any screen; kill horizontal overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/app/globals.css | 2 ++ src/components/GameTable.tsx | 48 +++++++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 9c50a20..c916049 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -42,6 +42,8 @@ html, body { height: 100%; + overflow-x: hidden; + overscroll-behavior: none; } body { diff --git a/src/components/GameTable.tsx b/src/components/GameTable.tsx index 6f5ab37..98df6d1 100644 --- a/src/components/GameTable.tsx +++ b/src/components/GameTable.tsx @@ -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 }) { 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 }) { 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 (
-
+
{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 ( 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" )} > );