Mobile UI polish pass (game-ui-design + mobile-app-ui-design)
CI/CD / CI - API (dotnet build + engine sim) (push) Failing after 1m40s
CI/CD / CI - Web (tsc + next build) (push) Failing after 1m19s
CI/CD / Deploy - local stack (db + server + web) (push) Has been skipped

Grounded in the two installed design skills:
- Safe-area insets (notch/home-bar): .safe-top/.safe-bottom/.safe-x helpers
  applied to the game-table HUD + bottom hand + reaction button, and to
  ScreenShell + HomeScreen (covers Profile/Shop/Leaderboard/etc.).
- Touch targets ≥44px: table HUD buttons (mute/forfeit/exit) and the reaction
  button now meet the 44/48px minimum.
- HUD readability: seat name/level labels (which float over the felt) get a
  text-shadow (.hud-shadow) and stronger contrast.

Verified: tsc + next build clean; web image rebuilt on :1500.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-05 06:38:46 +03:30
parent 1ea3b2b8d2
commit 7499c222e9
4 changed files with 24 additions and 12 deletions
+13
View File
@@ -59,6 +59,19 @@
box-shadow: 0 0 10px rgba(212, 175, 55, 0.45); box-shadow: 0 0 10px rgba(212, 175, 55, 0.45);
} }
/* HUD text stays legible over the dynamic felt/table (game-ui best practice). */
.hud-shadow {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6), 0 0 6px rgba(0, 0, 0, 0.4);
}
/* Safe-area helpers for notch / home-indicator on phones. */
.safe-top { padding-top: max(0.75rem, env(safe-area-inset-top)); }
.safe-bottom { padding-bottom: max(0.5rem, env(safe-area-inset-bottom)); }
.safe-x {
padding-left: max(0.75rem, env(safe-area-inset-left));
padding-right: max(0.75rem, env(safe-area-inset-right));
}
html, html,
body { body {
height: 100%; height: 100%;
+8 -9
View File
@@ -76,13 +76,13 @@ export function GameTable({
return ( return (
<main className="persian-pattern relative h-dvh w-full overflow-hidden"> <main className="persian-pattern relative h-dvh w-full overflow-hidden">
{/* Top HUD */} {/* Top HUD */}
<div className="absolute top-0 inset-x-0 z-30 flex items-start justify-between p-3 sm:p-4"> <div className="absolute top-0 inset-x-0 z-30 flex items-start justify-between gap-2 safe-top safe-x pb-3 sm:p-4">
<Scoreboard /> <Scoreboard />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{trump && <TrumpBadge trump={trump} />} {trump && <TrumpBadge trump={trump} />}
<button <button
onClick={toggleAll} onClick={toggleAll}
className="glass rounded-full p-2.5 hover:bg-navy-800 transition" className="glass rounded-full min-h-11 min-w-11 grid place-items-center hover:bg-navy-800 transition"
title={t("settings.audio")} title={t("settings.audio")}
> >
{muted ? ( {muted ? (
@@ -94,7 +94,7 @@ export function GameTable({
{onForfeit && ( {onForfeit && (
<button <button
onClick={() => setAskFf(true)} onClick={() => setAskFf(true)}
className="glass rounded-full p-2.5 hover:bg-navy-800 transition" className="glass rounded-full min-h-11 min-w-11 grid place-items-center hover:bg-navy-800 transition"
title={t("forfeit.title")} title={t("forfeit.title")}
> >
<Flag className="size-4 text-rose-300/90" /> <Flag className="size-4 text-rose-300/90" />
@@ -102,7 +102,7 @@ export function GameTable({
)} )}
<button <button
onClick={exit} onClick={exit}
className="glass rounded-full p-2.5 hover:bg-navy-800 transition" className="glass rounded-full min-h-11 min-w-11 grid place-items-center hover:bg-navy-800 transition"
title={t("hud.quit")} title={t("hud.quit")}
> >
<LogOut className="size-4 text-cream/80" /> <LogOut className="size-4 text-cream/80" />
@@ -293,10 +293,9 @@ function SeatAvatar({ seat, className }: { seat: Seat; className?: string }) {
<Crown className="absolute -top-3 size-4 text-gold-400 fill-gold-500" /> <Crown className="absolute -top-3 size-4 text-gold-400 fill-gold-500" />
)} )}
</motion.div> </motion.div>
<span className="text-[11px] text-cream/80 max-w-20 truncate">{name}</span> <span className="text-[11px] font-semibold text-cream max-w-20 truncate hud-shadow">{name}</span>
{sp && sp.level > 0 && ( {sp && sp.level > 0 && (
<span className="text-[9px] text-gold-400/80 leading-none"> <span className="text-[9px] text-gold-300 leading-none hud-shadow">
{team === 0 ? "" : ""}
{`Lv ${sp.level}`} {`Lv ${sp.level}`}
</span> </span>
)} )}
@@ -445,7 +444,7 @@ function PlayerHand({ legalIds }: { legalIds: Set<string> }) {
return ( return (
<div <div
className={cn( className={cn(
"absolute bottom-0 inset-x-0 flex justify-center pb-2 pointer-events-none", "absolute bottom-0 inset-x-0 flex justify-center safe-bottom pointer-events-none",
choosing ? "z-50" : "z-20" choosing ? "z-50" : "z-20"
)} )}
> >
@@ -704,7 +703,7 @@ function Reactions() {
{/* button */} {/* button */}
<button <button
onClick={() => setOpen((o) => !o)} onClick={() => setOpen((o) => !o)}
className="absolute bottom-4 ltr:right-4 rtl:left-4 z-50 glass rounded-full p-3 hover:bg-navy-800 transition" className="absolute bottom-[max(1rem,env(safe-area-inset-bottom))] ltr:right-4 rtl:left-4 z-50 glass rounded-full min-h-12 min-w-12 grid place-items-center hover:bg-navy-800 transition"
title={t("reactions.title")} title={t("reactions.title")}
> >
<SmilePlus className="size-5 text-gold-400" /> <SmilePlus className="size-5 text-gold-400" />
+1 -1
View File
@@ -46,7 +46,7 @@ export function HomeScreen() {
const playOnline = () => nav(isAuthed ? "online" : "auth"); const playOnline = () => nav(isAuthed ? "online" : "auth");
return ( return (
<main className="persian-pattern relative h-dvh w-full overflow-y-auto overscroll-contain"> <main className="persian-pattern relative h-dvh w-full overflow-y-auto overscroll-contain safe-top safe-x safe-bottom">
<FloatingSuits /> <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 p-4 sm:p-6 flex flex-col min-h-dvh">
<div className="pt-1"> <div className="pt-1">
+2 -2
View File
@@ -35,8 +35,8 @@ export function ScreenShell({ children }: { children: React.ReactNode }) {
// Fixed-height viewport scroller: body is `overflow:hidden` (for the game // Fixed-height viewport scroller: body is `overflow:hidden` (for the game
// table), so the shell must own its scroll (h-dvh + overflow-y-auto) — with // table), so the shell must own its scroll (h-dvh + overflow-y-auto) — with
// min-h-dvh the content just expands past the body and gets clipped. // min-h-dvh the content just expands past the body and gets clipped.
<main className="persian-pattern relative h-dvh w-full overflow-y-auto overscroll-contain"> <main className="persian-pattern relative h-dvh w-full overflow-y-auto overscroll-contain safe-top safe-x">
<div className="mx-auto w-full max-w-2xl p-4 pb-24 sm:p-6 sm:pb-28">{children}</div> <div className="mx-auto w-full max-w-2xl px-4 pt-3 pb-[max(6rem,calc(env(safe-area-inset-bottom)+5rem))] sm:px-6">{children}</div>
</main> </main>
); );
} }