UNO polish: center nav-rail items, drop per-page XP bar, shop category tabs
- 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:
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,16 +188,37 @@ export function ShopScreen() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sections.map((sec) => {
|
||||
{/* 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);
|
||||
if (!list.length) return null;
|
||||
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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user