UNO polish: center nav-rail items, drop per-page XP bar, shop category tabs
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 6m33s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m6s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m0s

- NavRail: vertically center items in the side rail (was top-aligned).
- ScreenHeader: showXp defaults off — the level/XP bar no longer clutters every
  sub-page (it lives on Home's chip + the Profile page).
- Shop: category tabs (avatars / fronts / backs / reactions / stickers / titles
  / XP) so only one category shows at a time — no more endless scroll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-11 11:53:10 +03:30
parent 34678c4e0e
commit 3d3241b976
4 changed files with 40 additions and 16 deletions
+1 -1
View File
@@ -39,7 +39,7 @@ export function NavRail({ bottom = false }: { bottom?: boolean }) {
: cn(
// portrait: bottom bar; landscape: side rail
"border-t border-gold/10 pb-[max(0.375rem,env(safe-area-inset-bottom))]",
"landscape:order-first landscape:h-full landscape:w-[78px] landscape:flex-col landscape:justify-start",
"landscape:order-first landscape:h-full landscape:w-[78px] landscape:flex-col landscape:justify-center",
"landscape:gap-1.5 landscape:py-3 landscape:pb-3 landscape:border-t-0",
"ltr:landscape:border-r rtl:landscape:border-l"
)
+2 -2
View File
@@ -10,12 +10,12 @@ export function ScreenHeader({
title,
back = "home",
right,
showXp = true,
showXp = false,
}: {
title: string;
back?: Screen;
right?: React.ReactNode;
/** Show the persistent level + XP chip beneath the header (default on). */
/** Show the persistent level + XP chip beneath the header (default off). */
showXp?: boolean;
}) {
const navBack = useUIStore((s) => s.back);
+35 -13
View File
@@ -78,6 +78,7 @@ export function ShopScreen() {
const [items, setItems] = useState<ShopItem[]>([]);
const [msg, setMsg] = useState("");
const [detail, setDetail] = useState<ShopItem | null>(null);
const [cat, setCat] = useState<ShopItem["kind"]>("avatar");
useEffect(() => {
getService().getShopItems().then(setItems);
@@ -187,19 +188,40 @@ export function ShopScreen() {
</div>
)}
{sections.map((sec) => {
const list = items.filter((i) => i.kind === sec.kind);
if (!list.length) return null;
return (
<Section key={sec.kind} title={sec.title} hint={sec.hint}>
<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)} />
))}
</div>
</Section>
);
})}
{/* category tabs */}
<div className="flex gap-2 overflow-x-auto pb-2 -mx-1 px-1 mb-3">
{sections.map((sec) => (
<button
key={sec.kind}
onClick={() => setCat(sec.kind)}
className={cn(
"shrink-0 rounded-full px-4 py-2 text-sm font-bold transition whitespace-nowrap",
cat === sec.kind ? "btn-gold" : "panel text-cream/70 hover:text-cream"
)}
>
{sec.title}
</button>
))}
</div>
{sections
.filter((sec) => sec.kind === cat)
.map((sec) => {
const list = items.filter((i) => i.kind === sec.kind);
return (
<Section key={sec.kind} title={sec.title} hint={sec.hint}>
{list.length === 0 ? (
<p className="text-center text-cream/40 text-sm py-8">{t("shop.emptyCat")}</p>
) : (
<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)} />
))}
</div>
)}
</Section>
);
})}
<AnimatePresence>
{detail && (
+2
View File
@@ -320,6 +320,7 @@ const fa: Dict = {
"shop.reqLevel": "سطح",
"shop.reqRating": "امتیاز",
"shop.reqAchv": "دستاورد:",
"shop.emptyCat": "آیتمی در این دسته نیست",
"reward.newTitle": "عنوان جدید",
"reactions.title": "شکلک",
@@ -666,6 +667,7 @@ const en: Dict = {
"shop.reqLevel": "Level",
"shop.reqRating": "Rating",
"shop.reqAchv": "Achievement:",
"shop.emptyCat": "No items in this category",
"reward.newTitle": "New title",
"reactions.title": "Emoji",