UNO refactor (stage 2): responsive list/grid screens + chat
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 46s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 51s

Make all menu screens use the width on desktop/landscape and the UNO panels:
- Shop item grid 3→up to 6 cols; BuyCoins packs 2→4 cols on lg.
- Lobby: panel league pick (2-col) + 2-col CTA buttons.
- Achievements / Notifications / Leaderboard / Friends lists → responsive
  grids (1 col mobile, 2 cols on lg); glass→panel on section containers.
- Chat: centered max-w-3xl column on desktop, green send button.
All responsive for mobile + desktop. tsc + build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-11 10:35:56 +03:30
parent 5c00f44fdc
commit ac05a7b679
8 changed files with 32 additions and 22 deletions
@@ -36,7 +36,7 @@ export function AchievementsScreen() {
<ScreenHeader title={t("achv.title")} />
{/* summary */}
<div className="glass rounded-2xl p-4 mb-4 flex items-center justify-around text-center">
<div className="panel rounded-2xl p-4 mb-4 flex items-center justify-around text-center">
<Stat value={`${unlockedCount}/${ACHIEVEMENTS.length}`} label={t("achv.unlocked")} />
<div className="h-8 w-px bg-cream/10" />
<Stat
@@ -74,7 +74,7 @@ export function AchievementsScreen() {
</div>
{/* achievement list */}
<div className="space-y-2 mb-6">
<div className="grid gap-2 lg:grid-cols-2 mb-6">
{list.map((a, i) => {
const prog = achievementProgress(a, stats, profile.rating, profile.level);
const unlocked = profile.unlocked.includes(a.id) || prog >= a.goal;
+1 -1
View File
@@ -128,7 +128,7 @@ export function BuyCoinsScreen() {
<div className="mb-4 text-center text-cream/80 text-sm glass rounded-xl py-2">{msg}</div>
)}
<div className="grid grid-cols-2 gap-3 pb-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 pb-6">
{packs.map((p) => (
<button
key={p.id}
+4 -2
View File
@@ -50,7 +50,8 @@ export function ChatScreen() {
};
return (
<main className="persian-pattern relative h-dvh w-full flex flex-col">
<main className="persian-pattern relative h-dvh w-full flex justify-center">
<div className="w-full max-w-3xl flex flex-col h-full">
{/* header */}
<header className="glass flex items-center gap-2 p-3 shrink-0 z-10 safe-top">
<button
@@ -115,12 +116,13 @@ export function ChatScreen() {
/>
<button
onClick={send}
className="btn-gold tap grid place-items-center rounded-full shrink-0"
className="btn-green tap grid place-items-center rounded-full shrink-0"
aria-label={t("chat.send")}
>
<Send className="size-4 rtl:-scale-x-100" />
</button>
</footer>
</div>
</main>
);
}
+15 -7
View File
@@ -54,7 +54,7 @@ export function FriendsScreen() {
<ScreenHeader title={t("social.title")} />
{/* tabs */}
<div className="glass rounded-2xl p-1 flex gap-1 mb-4">
<div className="panel rounded-2xl p-1 flex gap-1 mb-4">
<TabButton active={tab === "friends"} onClick={() => setTab("friends")} icon={<Users className="size-4" />} label={t("social.tabFriends")} badge={requests.length} />
<TabButton active={tab === "discover"} onClick={() => setTab("discover")} icon={<Search className="size-4" />} label={t("social.tabDiscover")} />
<TabButton active={tab === "messages"} onClick={() => setTab("messages")} icon={<MessageCircle className="size-4" />} label={t("social.tabMessages")} />
@@ -123,7 +123,7 @@ function FriendsTab() {
{requests.length > 0 && (
<div className="mb-4">
<h3 className="text-xs text-cream/55 mb-2">{t("friends.requests")}</h3>
<div className="space-y-2">
<div className="grid gap-2 lg:grid-cols-2">
{requests.map((r) => (
<div key={r.id} className="glass rounded-xl p-2.5 flex items-center gap-3">
<button onClick={() => viewProfile(r.from.id)} className="text-2xl active:scale-95 transition">
@@ -142,8 +142,12 @@ function FriendsTab() {
</div>
)}
<div className="space-y-2 pb-6">
{friends.length === 0 && <EmptyState icon={<Users className="size-7 text-gold-400/70" />} text={t("friends.empty")} />}
<div className="grid gap-2 lg:grid-cols-2 pb-6">
{friends.length === 0 && (
<div className="lg:col-span-2">
<EmptyState icon={<Users className="size-7 text-gold-400/70" />} text={t("friends.empty")} />
</div>
)}
{friends.map((f: Friend) => (
<div key={f.id} className="glass rounded-xl p-2.5 flex items-center gap-3">
<button onClick={() => viewProfile(f.id)} className="flex items-center gap-3 flex-1 min-w-0 text-start active:scale-[0.99] transition">
@@ -249,7 +253,7 @@ function DiscoverTab() {
<EmptyState icon={<Search className="size-7 text-gold-400/70" />} text={t("discover.noResults")} />
)}
<div className="space-y-2">
<div className="grid gap-2 lg:grid-cols-2">
{!loading && list?.map((p) => <DiscoverRow key={p.id} player={p} />)}
</div>
</div>
@@ -356,8 +360,12 @@ function MessagesTab() {
if (convs == null) return <div className="grid place-items-center py-10"><Loader2 className="size-6 text-gold-400 animate-spin" /></div>;
return (
<div className="space-y-2 pb-6">
{convs.length === 0 && <EmptyState icon={<MessageCircle className="size-7 text-gold-400/70" />} text={t("messages.empty")} />}
<div className="grid gap-2 lg:grid-cols-2 pb-6">
{convs.length === 0 && (
<div className="lg:col-span-2">
<EmptyState icon={<MessageCircle className="size-7 text-gold-400/70" />} text={t("messages.empty")} />
</div>
)}
{convs.map((c) => (
<button key={c.friend.id} onClick={() => open(c)} className="w-full glass rounded-xl p-2.5 flex items-center gap-3 text-start active:scale-[0.99] transition">
<div className="relative shrink-0">
+2 -2
View File
@@ -26,7 +26,7 @@ export function LeaderboardScreen() {
<ScreenHeader title={t("lead.title")} />
{leaderboard.length === 0 && (
<div className="space-y-1.5 pb-6">
<div className="grid gap-1.5 lg:grid-cols-2 pb-6">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="glass rounded-xl p-2.5 flex items-center gap-2.5 animate-pulse">
<span className="size-6 rounded bg-navy-800/80" />
@@ -41,7 +41,7 @@ export function LeaderboardScreen() {
</div>
)}
<div className="space-y-1.5 pb-6">
<div className="grid gap-1.5 lg:grid-cols-2 pb-6">
{leaderboard.map((e) => (
<button
key={e.id}
@@ -59,7 +59,7 @@ export function NotificationsScreen() {
</p>
)}
<div className="space-y-2 pb-6">
<div className="grid gap-2 lg:grid-cols-2 pb-6">
<AnimatePresence initial={false}>
{items.map((n) => (
<NotifRow
+5 -5
View File
@@ -67,12 +67,12 @@ export function OnlineLobbyScreen() {
<ScreenHeader title={t("lobby.title")} right={<CoinsPill />} />
{/* league pick (only for ranked) */}
<div className="glass rounded-2xl p-4 mb-4">
<div className="panel rounded-2xl p-4 mb-4">
<div className="flex items-center gap-1.5 text-sm text-cream/70 mb-2.5">
<Trophy className="size-4 text-gold-400" />
{t("lobby.chooseLeague")}
</div>
<div className="space-y-2">
<div className="grid gap-2 sm:grid-cols-2">
{MATCH_LEAGUES.map((l) => {
const locked = level < l.minLevel;
const active = l.id === leagueId;
@@ -123,11 +123,11 @@ export function OnlineLobbyScreen() {
)}
</div>
<div className="space-y-3">
<div className="grid gap-3 sm:grid-cols-2">
<motion.button
whileTap={{ scale: 0.985 }}
onClick={onRandom}
className="press-3d btn-gold w-full rounded-3xl p-5 flex items-center gap-4 text-start"
className="btn-gold w-full rounded-3xl p-5 flex items-center gap-4 text-start"
>
<span className="grid size-12 place-items-center rounded-2xl bg-black/15 text-[#2a1f04]">
<Trophy className="size-6" />
@@ -145,7 +145,7 @@ export function OnlineLobbyScreen() {
<motion.button
whileTap={{ scale: 0.985 }}
onClick={onCreate}
className="press-3d glass w-full rounded-3xl p-5 flex items-center gap-4 text-start"
className="press-3d panel w-full rounded-3xl p-5 flex items-center gap-4 text-start"
>
<span className="grid size-12 place-items-center rounded-2xl bg-teal-500/15 text-teal-300">
<Users className="size-6" />
+2 -2
View File
@@ -173,7 +173,7 @@ export function ShopScreen() {
{Array.from({ length: 2 }).map((_, s) => (
<div key={s}>
<div className="h-4 w-24 rounded bg-navy-800/80 animate-pulse mb-3" />
<div className="grid grid-cols-3 gap-3">
<div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 gap-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="glass rounded-2xl p-3 flex flex-col items-center gap-2 animate-pulse">
<div className="size-12 rounded-xl bg-navy-800/80" />
@@ -192,7 +192,7 @@ export function ShopScreen() {
if (!list.length) return null;
return (
<Section key={sec.kind} title={sec.title} hint={sec.hint}>
<div className="grid grid-cols-3 gap-3">
<div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 gap-3">
{list.map((item) => (
<ItemCard key={item.id} item={item} owned={owns(item)} reqLabel={lockLabel(item)} onOpen={() => setDetail(item)} />
))}