From 08d81cba65c65c581b351340124807d95664c5be Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 11 Jun 2026 01:56:52 +0330 Subject: [PATCH] UNO refactor (stage 1): hub shell with nav rail + internal-scroll panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuild ScreenShell into a UNO-style app shell: a persistent NavRail (vertical side rail in landscape, bottom tab bar in portrait — Home/Profile/Shop/Friends/ Leaderboard/Achievements, active highlighted gold) + a content panel that owns its own scroll so the page never scrolls as a whole and uses the width in landscape. Reskins all 10 menu screens at once. Transient screens (auth, matchmaking, room) opt out via hideNav. New nav.home i18n key. Co-Authored-By: Claude Opus 4.8 --- src/components/online/NavRail.tsx | 62 ++++++++++++++++++++ src/components/online/ScreenHeader.tsx | 28 +++++++-- src/components/screens/AuthScreen.tsx | 2 +- src/components/screens/MatchmakingScreen.tsx | 4 +- src/components/screens/RoomScreen.tsx | 2 +- src/lib/i18n.tsx | 2 + 6 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 src/components/online/NavRail.tsx diff --git a/src/components/online/NavRail.tsx b/src/components/online/NavRail.tsx new file mode 100644 index 0000000..6a0c001 --- /dev/null +++ b/src/components/online/NavRail.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { Home, ShoppingBag, Star, Trophy, User, Users } from "lucide-react"; +import { useUIStore, type Screen } from "@/lib/ui-store"; +import { useSessionStore } from "@/lib/session-store"; +import { useI18n } from "@/lib/i18n"; +import { cn } from "@/lib/cn"; + +type Item = { key: Screen; icon: typeof Home; label: string; authed?: boolean }; + +/** + * UNO-style primary navigation. A vertical rail pinned to the side in landscape + * (the app's main orientation) and a bottom tab bar in portrait. Active section + * is highlighted gold and pulled forward. + */ +export function NavRail() { + const screen = useUIStore((s) => s.screen); + const go = useUIStore((s) => s.go); + const isAuthed = useSessionStore((s) => s.isAuthed); + const { t } = useI18n(); + + const items: Item[] = [ + { key: "home", icon: Home, label: t("nav.home") }, + { key: "profile", icon: User, label: t("menu.profile") }, + { key: "shop", icon: ShoppingBag, label: t("menu.shop") }, + { key: "friends", icon: Users, label: t("menu.friends"), authed: true }, + { key: "leaderboard", icon: Trophy, label: t("menu.leaderboard") }, + { key: "achievements", icon: Star, label: t("achv.title") }, + ]; + + return ( + + ); +} diff --git a/src/components/online/ScreenHeader.tsx b/src/components/online/ScreenHeader.tsx index 4ff64e0..87f00d3 100644 --- a/src/components/online/ScreenHeader.tsx +++ b/src/components/online/ScreenHeader.tsx @@ -4,6 +4,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react"; import { useI18n } from "@/lib/i18n"; import { useUIStore, type Screen } from "@/lib/ui-store"; import { LevelXpBar } from "./LevelXpBar"; +import { NavRail } from "./NavRail"; export function ScreenHeader({ title, @@ -37,13 +38,28 @@ export function ScreenHeader({ ); } -export function ScreenShell({ children }: { children: React.ReactNode }) { +/** + * UNO-style app shell: a persistent {@link NavRail} (side rail in landscape / + * bottom bar in portrait) plus a content panel that owns its OWN scroll so the + * page never scrolls as a whole — lists scroll inside the panel and the chrome + * stays put. Set `hideNav` for transient/full-bleed screens (auth, matchmaking). + */ +export function ScreenShell({ + children, + hideNav = false, +}: { + children: React.ReactNode; + hideNav?: boolean; +}) { return ( - // 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. -
-
{children}
+
+ {/* content panel — scrolls internally, uses the width in landscape */} +
+
+ {children} +
+
+ {!hideNav && }
); } diff --git a/src/components/screens/AuthScreen.tsx b/src/components/screens/AuthScreen.tsx index 1c1022e..d3c47fa 100644 --- a/src/components/screens/AuthScreen.tsx +++ b/src/components/screens/AuthScreen.tsx @@ -14,7 +14,7 @@ export function AuthScreen() { const done = () => go("online"); return ( - +

{t("auth.subtitle")}

diff --git a/src/components/screens/MatchmakingScreen.tsx b/src/components/screens/MatchmakingScreen.tsx index df781f5..d0c17a4 100644 --- a/src/components/screens/MatchmakingScreen.tsx +++ b/src/components/screens/MatchmakingScreen.tsx @@ -69,7 +69,7 @@ export function MatchmakingScreen() { if (queued) { return ( - +

{t("queue.title")}

@@ -101,7 +101,7 @@ export function MatchmakingScreen() { } return ( - +
+