Mobile UI polish pass (game-ui-design + mobile-app-ui-design)
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:
@@ -59,6 +59,19 @@
|
||||
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,
|
||||
body {
|
||||
height: 100%;
|
||||
|
||||
@@ -76,13 +76,13 @@ export function GameTable({
|
||||
return (
|
||||
<main className="persian-pattern relative h-dvh w-full overflow-hidden">
|
||||
{/* 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 />
|
||||
<div className="flex items-center gap-2">
|
||||
{trump && <TrumpBadge trump={trump} />}
|
||||
<button
|
||||
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")}
|
||||
>
|
||||
{muted ? (
|
||||
@@ -94,7 +94,7 @@ export function GameTable({
|
||||
{onForfeit && (
|
||||
<button
|
||||
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")}
|
||||
>
|
||||
<Flag className="size-4 text-rose-300/90" />
|
||||
@@ -102,7 +102,7 @@ export function GameTable({
|
||||
)}
|
||||
<button
|
||||
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")}
|
||||
>
|
||||
<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" />
|
||||
)}
|
||||
</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 && (
|
||||
<span className="text-[9px] text-gold-400/80 leading-none">
|
||||
{team === 0 ? "" : ""}
|
||||
<span className="text-[9px] text-gold-300 leading-none hud-shadow">
|
||||
{`Lv ${sp.level}`}
|
||||
</span>
|
||||
)}
|
||||
@@ -445,7 +444,7 @@ function PlayerHand({ legalIds }: { legalIds: Set<string> }) {
|
||||
return (
|
||||
<div
|
||||
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"
|
||||
)}
|
||||
>
|
||||
@@ -704,7 +703,7 @@ function Reactions() {
|
||||
{/* button */}
|
||||
<button
|
||||
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")}
|
||||
>
|
||||
<SmilePlus className="size-5 text-gold-400" />
|
||||
|
||||
@@ -46,7 +46,7 @@ export function HomeScreen() {
|
||||
const playOnline = () => nav(isAuthed ? "online" : "auth");
|
||||
|
||||
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 />
|
||||
<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">
|
||||
|
||||
@@ -35,8 +35,8 @@ export function ScreenShell({ children }: { children: React.ReactNode }) {
|
||||
// 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
|
||||
// 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">
|
||||
<div className="mx-auto w-full max-w-2xl p-4 pb-24 sm:p-6 sm:pb-28">{children}</div>
|
||||
<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 px-4 pt-3 pb-[max(6rem,calc(env(safe-area-inset-bottom)+5rem))] sm:px-6">{children}</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user