Shop
Featured bundle with sale ribbons, gem packs, and the offer dock.
Best value−50%
Royal Bundle
King's Coffer
12,000
80
✦
5 boosts
Gems
80
500
1200
🎬
Free coinsWatch an ad · 150 coins
"use client";
import {
Avatar,
Body,
Bolt,
BottomStack,
BundleCell,
BundleCells,
Button,
Chrome,
ChromeRow,
Coin,
CornerRibbon,
Counter,
Counters,
Dim,
Dock,
FloatChrome,
Gem,
GroupKicker,
Heart,
LevelBadge,
MapPin,
MapScene,
Meter,
Pack,
PackRow,
Panel,
PageDots,
Podium,
PodiumColumn,
ProfileChip,
RankRow,
Ribbon,
Screen,
ScreenName,
SettingsRow,
ShopRow,
StarBurst,
Strike,
Tab,
TabBar,
TimerPill,
Toggle,
TopRibbon,
Slider,
Version,
Well,
Wordmark,
YouBadge,
} from "@objectifthunes/royal-games-ui";
import { useState, type ReactNode } from "react";
const ignoreAction = () => undefined;
interface ResourceCountersProps {
interactive?: boolean;
onAddCoins?: () => void;
onAddGems?: () => void;
}
interface NavigationProps {
onNavigate?: (value: string) => void;
}
function ResourceCounters({
interactive = true,
onAddCoins = ignoreAction,
onAddGems = ignoreAction,
}: ResourceCountersProps) {
return (
<Counters aria-label="Player resources">
{interactive ? (
<>
<Counter graphic={<Coin />} onAdd={onAddCoins} addLabel="Add coins">
2,450
</Counter>
<Counter graphic={<Gem />} onAdd={onAddGems} addLabel="Add gems">
12
</Counter>
</>
) : (
<>
<Counter graphic={<Coin />}>2,450</Counter>
<Counter graphic={<Gem />}>12</Counter>
</>
)}
</Counters>
);
}
interface RoyalChromeProps {
name: ReactNode;
onAddCoins?: () => void;
onAddGems?: () => void;
onOpenSettings?: () => void;
}
function RoyalChrome({
name,
onAddCoins = ignoreAction,
onAddGems = ignoreAction,
onOpenSettings = ignoreAction,
}: RoyalChromeProps) {
return (
<Chrome>
<ChromeRow tone="velvet">
<ProfileChip
avatar={
<Avatar decorative size="small">
♛
</Avatar>
}
name="Max"
caption="LEVEL 12"
/>
<ResourceCounters onAddCoins={onAddCoins} onAddGems={onAddGems} />
</ChromeRow>
<ChromeRow tone="stone">
<ScreenName>{name}</ScreenName>
<Button
tone="primary"
size="small"
iconOnly
aria-label="Settings"
onClick={onOpenSettings}
>
⚙
</Button>
</ChromeRow>
</Chrome>
);
}
interface RoyalTabsProps extends NavigationProps {
initial: string;
}
function RoyalTabs({ initial, onNavigate = ignoreAction }: RoyalTabsProps) {
const [tab, setTab] = useState(initial);
const handleTabChange = (value: string) => {
setTab(value);
onNavigate(value);
};
return (
<TabBar
aria-label="Primary navigation"
value={tab}
onValueChange={handleTabChange}
>
<Tab value="shop" icon="🛒">
Shop
</Tab>
<Tab value="ranks" icon="♛">
Ranks
</Tab>
<Tab value="home" icon="⌂">
Home
</Tab>
<Tab value="map" icon="⚑">
Map
</Tab>
<Tab value="more" icon="⚙" dot>
More
</Tab>
</TabBar>
);
}
// SCREEN_RECIPE: ShopScreen
export interface ShopScreenProps extends NavigationProps {
onBuyBundle?: () => void;
onBuyPack?: (pack: "80" | "500" | "1200") => void;
onWatchAd?: () => void;
}
/** Copy-ready commerce recipe with stable product and action columns. */
export function ShopScreen({
onBuyBundle = ignoreAction,
onBuyPack = ignoreAction,
onNavigate = ignoreAction,
onWatchAd = ignoreAction,
}: ShopScreenProps = {}) {
return (
<Screen aria-label="Royal Shop">
<Dock edge="top" tone="stone" density="compact">
<SettingsRow
variant="control"
label={<Ribbon size="screen">Royal Shop</Ribbon>}
control={<ResourceCounters interactive={false} />}
/>
</Dock>
<Body layout="commerce">
<Panel tone="paper">
<TopRibbon>
<Ribbon size="small">Best value</Ribbon>
</TopRibbon>
<CornerRibbon>
<Ribbon size="small">−50%</Ribbon>
</CornerRibbon>
<SettingsRow
icon="👑"
align="center"
divided={false}
label={
<>
<GroupKicker variant="compact">Royal Bundle</GroupKicker>
<ScreenName as="h2">King's Coffer</ScreenName>
</>
}
/>
<BundleCells>
<BundleCell label="12,000">
<Coin size="large" />
</BundleCell>
<BundleCell label="80">
<Gem size="large" />
</BundleCell>
<BundleCell label="5 boosts">✦</BundleCell>
</BundleCells>
<Button tone="gold" width="extended" onClick={onBuyBundle}>
<Strike>€19.99</Strike> €9.99 · BUY
</Button>
</Panel>
<Panel tone="primary">
<GroupKicker>Gems</GroupKicker>
<PackRow>
<Pack
art={<Gem size="large" />}
action={
<Button
tone="gold"
size="small"
onClick={() => onBuyPack("80")}
>
€1.99
</Button>
}
>
80
</Pack>
<Pack
art={<Gem size="large" />}
action={
<Button
tone="gold"
size="small"
onClick={() => onBuyPack("500")}
>
€8.99
</Button>
}
>
500
</Pack>
<Pack
art={<Gem size="large" />}
action={
<Button
tone="gold"
size="small"
onClick={() => onBuyPack("1200")}
>
€17.99
</Button>
}
>
1200
</Pack>
</PackRow>
</Panel>
</Body>
<BottomStack>
<Dock tone="primary" density="tight">
<ShopRow
art="🎬"
title="Free coins"
caption="Watch an ad · 150 coins"
action={
<Button tone="positive" size="small" onClick={onWatchAd}>
FREE
</Button>
}
/>
</Dock>
<RoyalTabs initial="shop" onNavigate={onNavigate} />
</BottomStack>
</Screen>
);
}This is the exact component rendered above. Offer ribbons remain anchored to their panels, product rows align independently of price text, and the body scrolls above the welded offer/navigation stack.