Ugame Ustore screen
import * as React from "react";
import { Image, ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Icon, type IconName, Text } from "@lumioui/ui";
import { cn, useHaptic } from "@lumioui/core";
import { catalogImg, HeroGradient, KenBurns, Mount, Press } from "../shared";
const NEW_RELEASES = [
{ title: "Horizon Forbidden West", price: "$39.99" },
{ title: "Red Dead Redemption", price: "$49.99" },
{ title: "Ghost of Tsushima", price: "$59.99" },
];
const POPULAR = [
{ title: "God of War Ragnarök", price: "$39.99", tag: "Action · Adventure" },
{ title: "Elden Ring", price: "$59.99", tag: "RPG · Open world" },
];
/** Floating dark tab capsule under everything — same idiom as the wallet
* movers nav so the two screens read as siblings. */
function GameNav() {
const insets = useSafeAreaInsets();
const items: { icon: IconName; label: string; active?: boolean }[] = [
{ icon: "home", label: "Home", active: true },
{ icon: "search", label: "Browse" },
{ icon: "book-open", label: "Library" },
{ icon: "user", label: "Profile" },
];
return (
<View
className="absolute inset-x-5 flex-row items-center rounded-full bg-[#141418] px-2 py-2"
style={{ bottom: Math.max(insets.bottom, 10) }}
>
{items.map((it) => (
<Press
key={it.label}
accessibilityRole="button"
accessibilityLabel={it.label}
className={cn(
"h-11 flex-1 flex-row items-center justify-center gap-1.5 rounded-full",
it.active && "bg-[#3A3A3C]",
)}
>
<Icon name={it.icon} size="sm" color={it.active ? "#FFFFFF" : "#8E8E93"} />
<Text className={cn("text-[12.5px]", it.active ? "font-semibold text-white" : "text-[#8E8E93]")}>
{it.label}
</Text>
</Press>
))}
</View>
);
}
/** Game store — dark catalog with a full-bleed key-art hero (chrome floats
* on the art), then the scrollable catalog: New Releases poster rail and
* Popular rows, dark pill nav floating at the bottom. */
export function GameStoreScreen({ onNotifications, onProfile }: { onNotifications?: () => void; onProfile?: () => void }) {
const haptics = useHaptic();
return (
<View className="flex-1 bg-[#0B0B0F]">
<ScrollView showsVerticalScrollIndicator={false} contentContainerClassName="pb-32">
{/* key-art hero — image underlaps the chrome */}
<View className="relative h-[430px] overflow-hidden">
<KenBurns>
<Image
source={{ uri: catalogImg("game", 900, 1100) }}
className="absolute inset-0"
resizeMode="cover"
accessibilityIgnoresInvertColors
/>
</KenBurns>
{/* bottom melt into the dark catalog */}
<View className="absolute inset-x-0 bottom-0 h-56">
<HeroGradient from="rgba(11,11,15,0)" to="#0B0B0F" />
</View>
<SafeAreaView edges={["top"]} className="flex-1">
<View className="flex-row items-center justify-between px-4 pt-1">
<Press onPress={onProfile} accessibilityRole="button" accessibilityLabel="Profile">
<Avatar fallback="JD" size="md" source={{ uri: "https://i.pravatar.cc/150?img=12" }} />
</Press>
<Press
onPress={onNotifications}
accessibilityRole="button"
accessibilityLabel="Notifications"
className="h-10 w-10 items-center justify-center rounded-full bg-black/40"
>
<Icon name="bell" size="sm" color="#FFFFFF" />
</Press>
</View>
<View className="flex-1" />
<Mount className="px-5 pb-5">
<Text className="text-center text-[34px] font-bold tracking-tight text-white">
The Last of Us
</Text>
<Text className="mt-1 text-center text-[13px] text-white/70">
A story of survival and hope
</Text>
<View className="mt-2.5 flex-row items-center justify-center gap-1.5">
<Text className="text-[12px] text-white/60">Reviews by</Text>
<View className="flex-row">
{[32, 47, 12].map((n, i) => (
<Avatar
key={n}
fallback="·"
size="sm"
source={{ uri: `https://i.pravatar.cc/64?img=${n}` }}
className={cn("border-2 border-[#0B0B0F]", i > 0 && "-ml-2")}
/>
))}
</View>
</View>
<View className="mt-4 flex-row items-center justify-center gap-3">
<Press
onPress={() => haptics.light()}
accessibilityRole="button"
className="rounded-full bg-white px-6 py-3"
>
<Text className="text-[14px] font-semibold text-[#0B0B0F]">View game ›</Text>
</Press>
<Press
onPress={() => haptics.selection()}
accessibilityRole="button"
accessibilityLabel="Add to wishlist"
className="flex-row items-center gap-1.5"
>
<Text className="text-[14px] font-medium text-white/85">Add to wishlist</Text>
<Icon name="bookmark" size="sm" color="#FFFFFF" />
</Press>
</View>
</Mount>
</SafeAreaView>
</View>
{/* catalog */}
<Mount delay={60} className="mt-2">
<View className="flex-row items-center justify-between px-5">
<Text className="text-[17px] font-bold text-white">New Releases</Text>
<Text className="text-[13px] text-white/45">See all</Text>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerClassName="mt-3.5 gap-4 px-5">
{NEW_RELEASES.map((g, i) => (
<Press key={g.title} accessibilityRole="button" className="w-40">
<Image
source={{ uri: catalogImg(i === 1 ? "controller" : "game", 360, 240) }}
className="h-24 w-40 rounded-2xl bg-white/10"
resizeMode="cover"
/>
<Text numberOfLines={1} className="mt-2 text-[14px] font-medium text-white">
{g.title}
</Text>
<Text className="mt-0.5 text-[13px] font-semibold text-white/60">{g.price}</Text>
</Press>
))}
</ScrollView>
</Mount>
<Mount delay={110} className="mt-6 px-5">
<View className="flex-row items-center justify-between">
<Text className="text-[17px] font-bold text-white">Popular now</Text>
<Text className="text-[13px] text-white/45">See all</Text>
</View>
{POPULAR.map((g) => (
<Press
key={g.title}
accessibilityRole="button"
className="mt-3.5 flex-row items-center rounded-3xl bg-white/[0.07] p-3.5"
>
<Image
source={{ uri: catalogImg("game", 200, 200) }}
className="h-14 w-14 rounded-2xl bg-white/10"
resizeMode="cover"
/>
<View className="ml-3.5 flex-1">
<Text className="text-[15px] font-semibold text-white">{g.title}</Text>
<Text className="mt-0.5 text-[12px] text-white/45">{g.tag}</Text>
</View>
<Text className="text-[15px] font-bold text-white">{g.price}</Text>
</Press>
))}
</Mount>
</ScrollView>
<GameNav />
</View>
);
}
GameStoreScreen.displayName = "GameStoreScreen";
Recreate the "Ugame Ustore Screen" screen in React Native using LumioUI.
Goal: Ugame Ustore screen Match the reference implementation's layout, behavior, and motion exactly — this is a production screen, not a mockup.
## Setup
Fastest path — install the screen plus its component deps:
```bash
npx lumioui add media-game-store
```
Or pull just the components: `npx lumioui add button text icon avatar`
Native packages:
- @lumioui/ui@latest
- @lumioui/core@latest
- react-native-safe-area-context@*
- react-native-reanimated@*
- react-native-svg@*
- nativewind@*
Stack assumptions: Expo SDK 54 / RN 0.81, Reanimated 4, NativeWind v4, TypeScript strict, Expo Go compatible.
## Components — how, where, when
### `button` — Tactile pill button with press spring, ink wash and async loading.
- imported but not rendered directly — likely used through a sibling or as a utility
### `text` — Themed type scale from caption to display.
- `<Text>` ×15
### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×3
### `avatar` — Circular user image with fallback initials and status dot.
- `<Avatar>` ×2
## Conventions (non-negotiable)
- Styling via NativeWind `className` with semantic tokens only: `bg-background`, `text-foreground`, `bg-card`, `text-card-foreground`, `bg-secondary`, `bg-muted`, `text-muted-foreground`, `bg-accent`, `bg-destructive`, `border-border`. Hard-coded colors only inside media overlays (`text-white`, `bg-black/N`).
- Motion via the token presets from `@lumioui/core` (`spring.press`, `spring.surface`, `spring.glide`, `timing.fast`, `timing.slow`, …) — no bespoke spring configs or durations.
- Gate transform motion behind `useReducedMotion()`; keep opacity/color feedback, drop travel and overshoot.
- Haptics via `useHaptic()`: `light` on press, `selection` on toggle/select, `warning` on invalid input. Never import expo-haptics directly.
- Press feedback via `usePressAnimation()` (~0.97 scale), not `activeOpacity` tricks.
- a11y: `accessibilityRole` + `accessibilityLabel` on every control, touch targets ≥44pt.
- Entrance choreography: staggered `Mount` wrappers (delay 0/60/120…) so regions land in sequence instead of popping in at once.
- Wrap the screen in `SafeAreaView`; keep dark mode working via the semantic tokens.
## Deliverable
One file: `screens/media/game-store.tsx` exporting GameStoreScreen. Preserve the props interface — the host app wires the callbacks (navigation, submit handlers) through them.
## Reference implementation
The LumioUI source for this screen — reproduce it faithfully:
```tsx
import * as React from "react";
import { Image, ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Icon, type IconName, Text } from "@lumioui/ui";
import { cn, useHaptic } from "@lumioui/core";
import { catalogImg, HeroGradient, KenBurns, Mount, Press } from "../shared";
const NEW_RELEASES = [
{ title: "Horizon Forbidden West", price: "$39.99" },
{ title: "Red Dead Redemption", price: "$49.99" },
{ title: "Ghost of Tsushima", price: "$59.99" },
];
const POPULAR = [
{ title: "God of War Ragnarök", price: "$39.99", tag: "Action · Adventure" },
{ title: "Elden Ring", price: "$59.99", tag: "RPG · Open world" },
];
/** Floating dark tab capsule under everything — same idiom as the wallet
* movers nav so the two screens read as siblings. */
function GameNav() {
const insets = useSafeAreaInsets();
const items: { icon: IconName; label: string; active?: boolean }[] = [
{ icon: "home", label: "Home", active: true },
{ icon: "search", label: "Browse" },
{ icon: "book-open", label: "Library" },
{ icon: "user", label: "Profile" },
];
return (
<View
className="absolute inset-x-5 flex-row items-center rounded-full bg-[#141418] px-2 py-2"
style={{ bottom: Math.max(insets.bottom, 10) }}
>
{items.map((it) => (
<Press
key={it.label}
accessibilityRole="button"
accessibilityLabel={it.label}
className={cn(
"h-11 flex-1 flex-row items-center justify-center gap-1.5 rounded-full",
it.active && "bg-[#3A3A3C]",
)}
>
<Icon name={it.icon} size="sm" color={it.active ? "#FFFFFF" : "#8E8E93"} />
<Text className={cn("text-[12.5px]", it.active ? "font-semibold text-white" : "text-[#8E8E93]")}>
{it.label}
</Text>
</Press>
))}
</View>
);
}
/** Game store — dark catalog with a full-bleed key-art hero (chrome floats
* on the art), then the scrollable catalog: New Releases poster rail and
* Popular rows, dark pill nav floating at the bottom. */
export function GameStoreScreen({ onNotifications, onProfile }: { onNotifications?: () => void; onProfile?: () => void }) {
const haptics = useHaptic();
return (
<View className="flex-1 bg-[#0B0B0F]">
<ScrollView showsVerticalScrollIndicator={false} contentContainerClassName="pb-32">
{/* key-art hero — image underlaps the chrome */}
<View className="relative h-[430px] overflow-hidden">
<KenBurns>
<Image
source={{ uri: catalogImg("game", 900, 1100) }}
className="absolute inset-0"
resizeMode="cover"
accessibilityIgnoresInvertColors
/>
</KenBurns>
{/* bottom melt into the dark catalog */}
<View className="absolute inset-x-0 bottom-0 h-56">
<HeroGradient from="rgba(11,11,15,0)" to="#0B0B0F" />
</View>
<SafeAreaView edges={["top"]} className="flex-1">
<View className="flex-row items-center justify-between px-4 pt-1">
<Press onPress={onProfile} accessibilityRole="button" accessibilityLabel="Profile">
<Avatar fallback="JD" size="md" source={{ uri: "https://i.pravatar.cc/150?img=12" }} />
</Press>
<Press
onPress={onNotifications}
accessibilityRole="button"
accessibilityLabel="Notifications"
className="h-10 w-10 items-center justify-center rounded-full bg-black/40"
>
<Icon name="bell" size="sm" color="#FFFFFF" />
</Press>
</View>
<View className="flex-1" />
<Mount className="px-5 pb-5">
<Text className="text-center text-[34px] font-bold tracking-tight text-white">
The Last of Us
</Text>
<Text className="mt-1 text-center text-[13px] text-white/70">
A story of survival and hope
</Text>
<View className="mt-2.5 flex-row items-center justify-center gap-1.5">
<Text className="text-[12px] text-white/60">Reviews by</Text>
<View className="flex-row">
{[32, 47, 12].map((n, i) => (
<Avatar
key={n}
fallback="·"
size="sm"
source={{ uri: `https://i.pravatar.cc/64?img=${n}` }}
className={cn("border-2 border-[#0B0B0F]", i > 0 && "-ml-2")}
/>
))}
</View>
</View>
<View className="mt-4 flex-row items-center justify-center gap-3">
<Press
onPress={() => haptics.light()}
accessibilityRole="button"
className="rounded-full bg-white px-6 py-3"
>
<Text className="text-[14px] font-semibold text-[#0B0B0F]">View game ›</Text>
</Press>
<Press
onPress={() => haptics.selection()}
accessibilityRole="button"
accessibilityLabel="Add to wishlist"
className="flex-row items-center gap-1.5"
>
<Text className="text-[14px] font-medium text-white/85">Add to wishlist</Text>
<Icon name="bookmark" size="sm" color="#FFFFFF" />
</Press>
</View>
</Mount>
</SafeAreaView>
</View>
{/* catalog */}
<Mount delay={60} className="mt-2">
<View className="flex-row items-center justify-between px-5">
<Text className="text-[17px] font-bold text-white">New Releases</Text>
<Text className="text-[13px] text-white/45">See all</Text>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerClassName="mt-3.5 gap-4 px-5">
{NEW_RELEASES.map((g, i) => (
<Press key={g.title} accessibilityRole="button" className="w-40">
<Image
source={{ uri: catalogImg(i === 1 ? "controller" : "game", 360, 240) }}
className="h-24 w-40 rounded-2xl bg-white/10"
resizeMode="cover"
/>
<Text numberOfLines={1} className="mt-2 text-[14px] font-medium text-white">
{g.title}
</Text>
<Text className="mt-0.5 text-[13px] font-semibold text-white/60">{g.price}</Text>
</Press>
))}
</ScrollView>
</Mount>
<Mount delay={110} className="mt-6 px-5">
<View className="flex-row items-center justify-between">
<Text className="text-[17px] font-bold text-white">Popular now</Text>
<Text className="text-[13px] text-white/45">See all</Text>
</View>
{POPULAR.map((g) => (
<Press
key={g.title}
accessibilityRole="button"
className="mt-3.5 flex-row items-center rounded-3xl bg-white/[0.07] p-3.5"
>
<Image
source={{ uri: catalogImg("game", 200, 200) }}
className="h-14 w-14 rounded-2xl bg-white/10"
resizeMode="cover"
/>
<View className="ml-3.5 flex-1">
<Text className="text-[15px] font-semibold text-white">{g.title}</Text>
<Text className="mt-0.5 text-[12px] text-white/45">{g.tag}</Text>
</View>
<Text className="text-[15px] font-bold text-white">{g.price}</Text>
</Press>
))}
</Mount>
</ScrollView>
<GameNav />
</View>
);
}
GameStoreScreen.displayName = "GameStoreScreen";
```
Screen: https://lumioui.dev/screens/media-game-store