Landscape: whole-app landscape-first + Home 2-column landscape layout
- Move orientation lock + RotatePrompt to app root → whole app is landscape- first now (UNO-style), not just the game. Generalized rotate copy. - Home: portrait unchanged; in landscape it becomes a 2-column app layout (col A = branding + play actions, col B = tiles + footer) that fits the short height with no scroll (landscape: Tailwind variants, overflow-hidden). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -73,18 +73,24 @@ export function HomeScreen() {
|
||||
const Chevron = locale === "fa" ? ChevronLeft : ChevronRight;
|
||||
|
||||
return (
|
||||
<main className="persian-pattern relative h-dvh w-full overflow-y-auto overscroll-contain safe-top safe-x safe-bottom">
|
||||
<main className="persian-pattern relative h-dvh w-full overflow-y-auto landscape:overflow-hidden overscroll-contain safe-top safe-x safe-bottom">
|
||||
<FloatingSuits />
|
||||
<div className="relative z-10 mx-auto w-full max-w-md p-4 sm:p-6 flex flex-col min-h-dvh">
|
||||
<div className="relative z-10 mx-auto w-full max-w-md landscape:max-w-5xl p-4 sm:p-6 flex flex-col min-h-dvh landscape:min-h-0 landscape:h-dvh">
|
||||
<div className="pt-1">
|
||||
<TopBar />
|
||||
</div>
|
||||
|
||||
{/* content: single column (portrait) → two columns (landscape) */}
|
||||
<div className="flex-1 flex flex-col min-h-0 landscape:flex-row landscape:items-stretch landscape:justify-center landscape:gap-6">
|
||||
|
||||
{/* ===== column A: branding + play actions ===== */}
|
||||
<div className="flex flex-col landscape:flex-1 landscape:max-w-md landscape:justify-center landscape:min-h-0">
|
||||
|
||||
{/* logo */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex flex-col items-center mt-4 mb-5 sm:mt-6 sm:mb-7"
|
||||
className="flex flex-col items-center mt-4 mb-5 sm:mt-6 sm:mb-7 landscape:mt-0 landscape:mb-3"
|
||||
>
|
||||
{/* logo + title on one row (no overflow); subtitle beneath the title */}
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -159,19 +165,24 @@ export function HomeScreen() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* ===== end column A ===== */}
|
||||
</div>
|
||||
|
||||
{/* ===== column B: tiles + footer ===== */}
|
||||
<div className="flex flex-col landscape:flex-1 landscape:max-w-sm landscape:justify-center landscape:min-h-0">
|
||||
|
||||
{/* tiles */}
|
||||
<div className="grid grid-cols-4 gap-2.5 mt-3 sm:mt-4">
|
||||
<div className="grid grid-cols-4 gap-2.5 mt-3 sm:mt-4 landscape:mt-0">
|
||||
<Tile icon={<User className="size-5" />} label={t("menu.profile")} tint="teal" onClick={() => nav("profile")} />
|
||||
<Tile icon={<Users className="size-5" />} label={t("menu.friends")} tint="sky" onClick={() => nav(isAuthed ? "friends" : "auth")} />
|
||||
<Tile icon={<Trophy className="size-5" />} label={t("menu.leaderboard")} tint="gold" onClick={() => nav("leaderboard")} />
|
||||
<Tile icon={<ShoppingBag className="size-5" />} label={t("menu.shop")} tint="rose" onClick={() => nav("shop")} />
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
<div className="flex-1 landscape:hidden" />
|
||||
|
||||
{/* footer */}
|
||||
<div className="flex items-center justify-between gap-2 pt-4 sm:pt-6 pb-2">
|
||||
<div className="flex items-center justify-between gap-2 pt-4 sm:pt-6 pb-2 landscape:pt-0 landscape:mt-4">
|
||||
{isAuthed ? (
|
||||
<button
|
||||
onClick={signOut}
|
||||
@@ -197,6 +208,10 @@ export function HomeScreen() {
|
||||
{t("home.lang")}
|
||||
</button>
|
||||
</div>
|
||||
{/* ===== end column B ===== */}
|
||||
</div>
|
||||
{/* ===== end content columns ===== */}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { GameTable } from "@/components/GameTable";
|
||||
import { PostMatchRewardsModal } from "@/components/online/PostMatchRewardsModal";
|
||||
import { MatchIntroOverlay } from "@/components/online/MatchIntroOverlay";
|
||||
import { RotatePrompt } from "@/components/online/RotatePrompt";
|
||||
import { useGameStore } from "@/lib/game-store";
|
||||
import { useSessionStore } from "@/lib/session-store";
|
||||
import { useUIStore } from "@/lib/ui-store";
|
||||
@@ -59,20 +58,6 @@ export function GameScreen() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Landscape-first table: best-effort lock to landscape on Android/PWA (iOS &
|
||||
// desktop reject it — harmless). Restored to portrait/auto when leaving.
|
||||
useEffect(() => {
|
||||
const o = (screen as unknown as { orientation?: { lock?: (m: string) => Promise<void>; unlock?: () => void } }).orientation;
|
||||
o?.lock?.("landscape").catch(() => {});
|
||||
return () => {
|
||||
try {
|
||||
o?.unlock?.();
|
||||
} catch {
|
||||
/* unsupported — ignore */
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const notifyAchievements = (r: RewardResult) => {
|
||||
for (const a of r.newAchievements)
|
||||
pushNotification({
|
||||
@@ -157,9 +142,6 @@ export function GameScreen() {
|
||||
onForfeit={canForfeit ? () => useGameStore.getState().forfeit() : undefined}
|
||||
/>
|
||||
|
||||
{/* Landscape-first: nudge to rotate when held in portrait on a phone */}
|
||||
<RotatePrompt />
|
||||
|
||||
{/* UNO-style "players joining the table" intro (online matches, once) */}
|
||||
<AnimatePresence>
|
||||
{introPending && mode === "online" && (
|
||||
|
||||
Reference in New Issue
Block a user