ui: unified rounded navbar everywhere, vertical home actions, no bot disconnect spam
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 8m9s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 57s

- NavRail: one rounded "pill" tab bar on every screen (matches home). ScreenShell
  lays out as a portrait column and floats the nav with margins + safe-area;
  dropped the landscape side-rail variant.
- Home: the three mode cards now stack vertically as full-width rows (portrait
  friendly) instead of a 3-up landscape row.
- Disconnect: removed the simulated random opponent "disconnect" in local games
  (DISCONNECT_CHANCE) and the in-game DisconnectBanner — bots/filled seats just
  auto-play their turn; no message, no pause. (Live reconnect grace still tracked
  internally but no longer shows a banner.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-12 01:12:26 +03:30
parent 55c0407d73
commit a7c0900c3b
5 changed files with 39 additions and 86 deletions
+1 -29
View File
@@ -1,7 +1,7 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { Crown, Flag, LogOut, SmilePlus, Volume2, VolumeX, WifiOff, Zap } from "lucide-react";
import { Crown, Flag, LogOut, SmilePlus, Volume2, VolumeX, Zap } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useGameStore } from "@/lib/game-store";
import { useSoundStore } from "@/lib/sound-store";
@@ -210,7 +210,6 @@ export function GameTable({
<TurnTimer />
<TurnIndicator />
</div>
<DisconnectBanner />
<Reactions />
<ShortcutsHint />
@@ -734,33 +733,6 @@ function TurnTimer() {
);
}
function DisconnectBanner() {
const seat = useGameStore((s) => s.disconnectedSeat);
const deadline = useGameStore((s) => s.reconnectDeadline);
const name = useGameStore((s) => (seat != null ? s.seatPlayers[seat]?.name : null));
const secs = useCountdown(deadline);
const { t } = useI18n();
return (
<AnimatePresence>
{seat != null && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-40"
>
<div className="glass rounded-2xl px-4 py-3 flex items-center gap-2.5 text-sm">
<WifiOff className="size-5 text-rose-300 animate-pulse" />
<span className="text-cream/90">
{t("dc.waiting", { name: name ?? "", s: secs ?? 0 })}
</span>
</div>
</motion.div>
)}
</AnimatePresence>
);
}
/* ----------------------------- Reactions ------------------------------ */
const REACTION_POS: Record<number, string> = {