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(
|
: cn(
|
||||||
// portrait: bottom bar; landscape: side rail
|
// portrait: bottom bar; landscape: side rail
|
||||||
"border-t border-gold/10 pb-[max(0.375rem,env(safe-area-inset-bottom))]",
|
"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",
|
"landscape:gap-1.5 landscape:py-3 landscape:pb-3 landscape:border-t-0",
|
||||||
"ltr:landscape:border-r rtl:landscape:border-l"
|
"ltr:landscape:border-r rtl:landscape:border-l"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ export function ScreenHeader({
|
|||||||
title,
|
title,
|
||||||
back = "home",
|
back = "home",
|
||||||
right,
|
right,
|
||||||
showXp = true,
|
showXp = false,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
back?: Screen;
|
back?: Screen;
|
||||||
right?: React.ReactNode;
|
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;
|
showXp?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const navBack = useUIStore((s) => s.back);
|
const navBack = useUIStore((s) => s.back);
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export function ShopScreen() {
|
|||||||
const [items, setItems] = useState<ShopItem[]>([]);
|
const [items, setItems] = useState<ShopItem[]>([]);
|
||||||
const [msg, setMsg] = useState("");
|
const [msg, setMsg] = useState("");
|
||||||
const [detail, setDetail] = useState<ShopItem | null>(null);
|
const [detail, setDetail] = useState<ShopItem | null>(null);
|
||||||
|
const [cat, setCat] = useState<ShopItem["kind"]>("avatar");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getService().getShopItems().then(setItems);
|
getService().getShopItems().then(setItems);
|
||||||
@@ -187,19 +188,40 @@ export function ShopScreen() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sections.map((sec) => {
|
{/* category tabs */}
|
||||||
const list = items.filter((i) => i.kind === sec.kind);
|
<div className="flex gap-2 overflow-x-auto pb-2 -mx-1 px-1 mb-3">
|
||||||
if (!list.length) return null;
|
{sections.map((sec) => (
|
||||||
return (
|
<button
|
||||||
<Section key={sec.kind} title={sec.title} hint={sec.hint}>
|
key={sec.kind}
|
||||||
<div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 gap-3">
|
onClick={() => setCat(sec.kind)}
|
||||||
{list.map((item) => (
|
className={cn(
|
||||||
<ItemCard key={item.id} item={item} owned={owns(item)} reqLabel={lockLabel(item)} onOpen={() => setDetail(item)} />
|
"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"
|
||||||
</div>
|
)}
|
||||||
</Section>
|
>
|
||||||
);
|
{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>
|
<AnimatePresence>
|
||||||
{detail && (
|
{detail && (
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ const fa: Dict = {
|
|||||||
"shop.reqLevel": "سطح",
|
"shop.reqLevel": "سطح",
|
||||||
"shop.reqRating": "امتیاز",
|
"shop.reqRating": "امتیاز",
|
||||||
"shop.reqAchv": "دستاورد:",
|
"shop.reqAchv": "دستاورد:",
|
||||||
|
"shop.emptyCat": "آیتمی در این دسته نیست",
|
||||||
"reward.newTitle": "عنوان جدید",
|
"reward.newTitle": "عنوان جدید",
|
||||||
|
|
||||||
"reactions.title": "شکلک",
|
"reactions.title": "شکلک",
|
||||||
@@ -666,6 +667,7 @@ const en: Dict = {
|
|||||||
"shop.reqLevel": "Level",
|
"shop.reqLevel": "Level",
|
||||||
"shop.reqRating": "Rating",
|
"shop.reqRating": "Rating",
|
||||||
"shop.reqAchv": "Achievement:",
|
"shop.reqAchv": "Achievement:",
|
||||||
|
"shop.emptyCat": "No items in this category",
|
||||||
"reward.newTitle": "New title",
|
"reward.newTitle": "New title",
|
||||||
|
|
||||||
"reactions.title": "Emoji",
|
"reactions.title": "Emoji",
|
||||||
|
|||||||
Reference in New Issue
Block a user