@rpg-engine/long-bow 0.8.181 → 0.8.184

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/components/Marketplace/CharacterListingForm.d.ts +15 -0
  2. package/dist/components/Marketplace/CharacterListingModal.d.ts +17 -0
  3. package/dist/components/Marketplace/CharacterMarketplacePanel.d.ts +22 -0
  4. package/dist/components/Marketplace/CharacterMarketplaceRows.d.ts +26 -0
  5. package/dist/components/Marketplace/Marketplace.d.ts +20 -1
  6. package/dist/components/Marketplace/MyCharacterListingsPanel.d.ts +19 -0
  7. package/dist/components/shared/DCRateStrip.d.ts +2 -0
  8. package/dist/components/shared/RadioOption.d.ts +22 -0
  9. package/dist/index.d.ts +4 -0
  10. package/dist/long-bow.cjs.development.js +1114 -130
  11. package/dist/long-bow.cjs.development.js.map +1 -1
  12. package/dist/long-bow.cjs.production.min.js +1 -1
  13. package/dist/long-bow.cjs.production.min.js.map +1 -1
  14. package/dist/long-bow.esm.js +1133 -154
  15. package/dist/long-bow.esm.js.map +1 -1
  16. package/dist/stories/Features/marketplace/CharacterListingModal.stories.d.ts +8 -0
  17. package/dist/stories/Features/marketplace/CharacterMarketplace.stories.d.ts +10 -0
  18. package/dist/stories/shared/RadioOption.stories.d.ts +8 -0
  19. package/package.json +1 -1
  20. package/src/components/DCWallet/DCWalletContent.tsx +5 -47
  21. package/src/components/Marketplace/BuyPanel.tsx +1 -0
  22. package/src/components/Marketplace/CharacterListingForm.tsx +102 -0
  23. package/src/components/Marketplace/CharacterListingModal.tsx +404 -0
  24. package/src/components/Marketplace/CharacterMarketplacePanel.tsx +450 -0
  25. package/src/components/Marketplace/CharacterMarketplaceRows.tsx +265 -0
  26. package/src/components/Marketplace/GroupedRowContainer.tsx +3 -1
  27. package/src/components/Marketplace/ManagmentPanel.tsx +1 -0
  28. package/src/components/Marketplace/Marketplace.tsx +163 -2
  29. package/src/components/Marketplace/MyCharacterListingsPanel.tsx +327 -0
  30. package/src/components/shared/DCRateStrip.tsx +67 -0
  31. package/src/components/shared/ItemRowWrapper.tsx +3 -1
  32. package/src/components/shared/RadioOption.tsx +93 -0
  33. package/src/index.tsx +4 -0
  34. package/src/stories/Features/marketplace/CharacterListingModal.stories.tsx +131 -0
  35. package/src/stories/Features/marketplace/CharacterMarketplace.stories.tsx +340 -0
  36. package/src/stories/shared/RadioOption.stories.tsx +93 -0
@@ -39,16 +39,18 @@ export const GroupedRowContainer: React.FC<IGroupedRowContainerProps> = ({
39
39
 
40
40
  const GroupWrapper = styled.div`
41
41
  margin-bottom: 2px;
42
+ overflow: hidden;
42
43
  `;
43
44
 
44
45
  const GroupHeader = styled.div<{ $clickable: boolean }>`
45
46
  position: relative;
46
47
  cursor: ${({ $clickable }) => ($clickable ? 'pointer' : 'default')};
48
+ overflow-x: hidden;
47
49
  `;
48
50
 
49
51
  const GroupMeta = styled.div`
50
52
  position: absolute;
51
- right: 180px;
53
+ right: 100px;
52
54
  top: 50%;
53
55
  transform: translateY(-50%);
54
56
  display: flex;
@@ -342,6 +342,7 @@ const BalanceCurrencyLabel = styled.span`
342
342
 
343
343
  const ItemComponentScrollWrapper = styled.div`
344
344
  overflow-y: scroll;
345
+ overflow-x: hidden;
345
346
  height: 335px;
346
347
  width: 95%;
347
348
  margin: 1rem auto 0 auto;
@@ -1,4 +1,6 @@
1
1
  import {
2
+ ICharacter,
3
+ ICharacterListing,
2
4
  IEquipmentSet,
3
5
  IItem,
4
6
  IMarketplaceBlueprintSearchRequest,
@@ -12,6 +14,7 @@ import { Settings2 } from 'pixelarticons/react/Settings2';
12
14
  import { ShoppingBag } from 'pixelarticons/react/ShoppingBag';
13
15
  import { ShoppingCart } from 'pixelarticons/react/ShoppingCart';
14
16
  import { Store } from 'pixelarticons/react/Store';
17
+ import { User } from 'pixelarticons/react/User';
15
18
  import { Wallet } from 'pixelarticons/react/Wallet';
16
19
  import React, { useState } from 'react';
17
20
  import styled from 'styled-components';
@@ -27,6 +30,9 @@ import { HistoryPanel } from './HistoryPanel';
27
30
  import { ManagmentPanel } from './ManagmentPanel';
28
31
  import { MarketplacePaymentMethod } from './MarketplaceBuyModal';
29
32
  import { MarketplaceAcceptedCurrency, MarketplaceSettingsPanel } from './MarketplaceSettingsPanel';
33
+ import { CharacterMarketplacePanel } from './CharacterMarketplacePanel';
34
+ import { MyCharacterListingsPanel } from './MyCharacterListingsPanel';
35
+ import { CharacterListingForm } from './CharacterListingForm';
30
36
 
31
37
  export interface IMarketPlaceProps {
32
38
  items: IMarketplaceItem[];
@@ -110,9 +116,30 @@ export interface IMarketPlaceProps {
110
116
  // Wallet tab
111
117
  walletProps?: IDCWalletContentProps;
112
118
  showWalletTab?: boolean;
119
+
120
+ /** Entities atlas for character sprites (separate from the items atlasIMG/atlasJSON) */
121
+ characterAtlasIMG?: any;
122
+ characterAtlasJSON?: any;
123
+ // Character marketplace props
124
+ characterListings?: ICharacterListing[];
125
+ characterListingsTotal?: number;
126
+ characterListingsPage?: number;
127
+ characterListingsItemsPerPage?: number;
128
+ onCharacterListingsPageChange?: (page: number) => void;
129
+ onCharacterBuy?: (listingId: string) => void;
130
+ myCharacterListings?: ICharacterListing[];
131
+ myCharacterListingsTotal?: number;
132
+ myCharacterListingsPage?: number;
133
+ onMyCharacterListingsPageChange?: (page: number) => void;
134
+ onCharacterList?: (characterId: string, price: number) => void;
135
+ onCharacterDelist?: (listingId: string) => void;
136
+ accountCharacters?: ICharacter[];
137
+ characterListingsLoading?: boolean;
138
+ characterNameFilter?: string;
139
+ onCharacterNameFilterChange?: (name: string) => void;
113
140
  }
114
141
 
115
- type ActiveTab = 'marketplace' | 'sell' | 'buy-orders' | 'history' | 'wallet' | 'settings';
142
+ type ActiveTab = 'marketplace' | 'characters' | 'sell' | 'buy-orders' | 'history' | 'wallet' | 'settings';
116
143
 
117
144
  export const Marketplace: React.FC<IMarketPlaceProps> = props => {
118
145
  const {
@@ -155,11 +182,31 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
155
182
  // Wallet
156
183
  walletProps,
157
184
  showWalletTab = true,
185
+ // Character marketplace
186
+ characterListings = [],
187
+ characterListingsTotal = 0,
188
+ characterListingsPage = 1,
189
+ characterListingsItemsPerPage = 8,
190
+ onCharacterListingsPageChange,
191
+ onCharacterBuy,
192
+ myCharacterListings = [],
193
+ myCharacterListingsTotal = 0,
194
+ myCharacterListingsPage = 1,
195
+ onMyCharacterListingsPageChange,
196
+ onCharacterDelist,
197
+ onCharacterList,
198
+ accountCharacters = [],
199
+ characterListingsLoading = false,
200
+ characterNameFilter = '',
201
+ onCharacterNameFilterChange,
202
+ characterAtlasIMG,
203
+ characterAtlasJSON,
158
204
  } = props;
159
205
 
160
206
  const [activeTab, setActiveTab] = useState<ActiveTab>('marketplace');
161
207
  const [acceptedCurrency, setAcceptedCurrency] = useState<MarketplaceAcceptedCurrency>(acceptedCurrencyProp ?? MarketplaceAcceptedCurrency.GoldOrDc);
162
208
  const [isBlueprintSearchOpen, setIsBlueprintSearchOpen] = useState(false);
209
+ const [characterSubTab, setCharacterSubTab] = useState<'browse' | 'my-listings' | 'list'>('browse');
163
210
 
164
211
  const handleCurrencyChange = (value: MarketplaceAcceptedCurrency) => {
165
212
  setAcceptedCurrency(value);
@@ -187,7 +234,7 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
187
234
  onCloseButton={() => {
188
235
  if (onClose) onClose();
189
236
  }}
190
- width="800px"
237
+ width="920px"
191
238
  cancelDrag="#MarketContainer, .rpgui-dropdown-imp, input, .empty-slot, button"
192
239
  scale={scale}
193
240
  >
@@ -208,6 +255,11 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
208
255
  label: 'Buy',
209
256
  icon: <ShoppingCart width={18} height={18} />,
210
257
  },
258
+ {
259
+ id: 'characters',
260
+ label: 'Characters',
261
+ icon: <User width={18} height={18} />,
262
+ },
211
263
  {
212
264
  id: 'history',
213
265
  label: 'History',
@@ -236,6 +288,84 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
236
288
  <BuyPanel {...props} />
237
289
  )}
238
290
 
291
+ {activeTab === 'characters' && (
292
+ <>
293
+ <CharacterSubTabs>
294
+ <CharacterSubTab
295
+ $active={characterSubTab === 'browse'}
296
+ onClick={() => setCharacterSubTab('browse')}
297
+ type="button"
298
+ >
299
+ Browse
300
+ </CharacterSubTab>
301
+ <CharacterSubTab
302
+ $active={characterSubTab === 'my-listings'}
303
+ onClick={() => setCharacterSubTab('my-listings')}
304
+ type="button"
305
+ >
306
+ My Listings
307
+ </CharacterSubTab>
308
+ <CharacterSubTab
309
+ $active={characterSubTab === 'list'}
310
+ onClick={() => setCharacterSubTab('list')}
311
+ type="button"
312
+ >
313
+ List Character
314
+ </CharacterSubTab>
315
+ </CharacterSubTabs>
316
+
317
+ {characterSubTab === 'browse' && (
318
+ <CharacterMarketplacePanel
319
+ characterListings={characterListings ?? []}
320
+ totalCount={characterListingsTotal ?? 0}
321
+ currentPage={characterListingsPage ?? 1}
322
+ itemsPerPage={characterListingsItemsPerPage ?? 10}
323
+ onPageChange={onCharacterListingsPageChange ?? (() => {})}
324
+ onCharacterBuy={onCharacterBuy ?? (() => {})}
325
+ atlasJSON={props.atlasJSON}
326
+ atlasIMG={props.atlasIMG}
327
+ characterAtlasJSON={characterAtlasJSON ?? props.atlasJSON}
328
+ characterAtlasIMG={characterAtlasIMG ?? props.atlasIMG}
329
+ enableHotkeys={props.enableHotkeys}
330
+ disableHotkeys={props.disableHotkeys}
331
+ nameFilter={characterNameFilter}
332
+ onNameFilterChange={onCharacterNameFilterChange}
333
+ isLoading={characterListingsLoading}
334
+ />
335
+ )}
336
+
337
+ {characterSubTab === 'my-listings' && (
338
+ <MyCharacterListingsPanel
339
+ myCharacterListings={myCharacterListings ?? []}
340
+ totalCount={myCharacterListingsTotal ?? 0}
341
+ currentPage={myCharacterListingsPage ?? 1}
342
+ itemsPerPage={10}
343
+ onPageChange={onMyCharacterListingsPageChange ?? (() => {})}
344
+ onCharacterDelist={onCharacterDelist ?? (() => {})}
345
+ atlasJSON={props.atlasJSON}
346
+ atlasIMG={props.atlasIMG}
347
+ characterAtlasJSON={characterAtlasJSON ?? props.atlasJSON}
348
+ characterAtlasIMG={characterAtlasIMG ?? props.atlasIMG}
349
+ enableHotkeys={props.enableHotkeys}
350
+ disableHotkeys={props.disableHotkeys}
351
+ />
352
+ )}
353
+
354
+ {characterSubTab === 'list' && (
355
+ <CharacterListingForm
356
+ accountCharacters={accountCharacters ?? []}
357
+ onCharacterList={onCharacterList ?? (() => {})}
358
+ atlasJSON={props.atlasJSON}
359
+ atlasIMG={props.atlasIMG}
360
+ characterAtlasJSON={characterAtlasJSON ?? props.atlasJSON}
361
+ characterAtlasIMG={characterAtlasIMG ?? props.atlasIMG}
362
+ enableHotkeys={props.enableHotkeys}
363
+ disableHotkeys={props.disableHotkeys}
364
+ />
365
+ )}
366
+ </>
367
+ )}
368
+
239
369
  {activeTab === 'sell' && (
240
370
  <ManagmentPanel {...props} acceptedCurrency={acceptedCurrency} />
241
371
  )}
@@ -323,3 +453,34 @@ const PagerContainer = styled.div`
323
453
  margin: 6px auto 0 auto;
324
454
  padding: 4px 10px;
325
455
  `;
456
+
457
+ const CharacterSubTabs = styled.div`
458
+ display: flex;
459
+ gap: 8px;
460
+ width: 95%;
461
+ margin: 10px auto 0 auto;
462
+ padding: 8px;
463
+ background: rgba(0, 0, 0, 0.2);
464
+ border-radius: 4px;
465
+ border: 1px solid rgba(255, 255, 255, 0.05);
466
+ `;
467
+
468
+ const CharacterSubTab = styled.button<{ $active: boolean }>`
469
+ flex: 1;
470
+ padding: 8px 12px;
471
+ font-family: 'Press Start 2P', cursive;
472
+ font-size: 0.45rem;
473
+ border-radius: 4px;
474
+ border: 1px solid ${({ $active }) => ($active ? '#f59e0b' : 'rgba(255,255,255,0.12)')};
475
+ background: ${({ $active }) => ($active ? 'rgba(245,158,11,0.15)' : 'rgba(0,0,0,0.3)')};
476
+ color: ${({ $active }) => ($active ? '#f59e0b' : '#777')};
477
+ cursor: pointer;
478
+ transition: border-color 0.15s, background 0.15s, color 0.15s;
479
+ text-transform: uppercase;
480
+ letter-spacing: 0.5px;
481
+
482
+ &:hover {
483
+ border-color: ${({ $active }) => ($active ? '#f59e0b' : 'rgba(255,255,255,0.3)')};
484
+ color: ${({ $active }) => ($active ? '#f59e0b' : '#bbb')};
485
+ }
486
+ `;
@@ -0,0 +1,327 @@
1
+ import { formatDCAmount, ICharacterListing } from '@rpg-engine/shared';
2
+ import { Delete } from 'pixelarticons/react/Delete';
3
+ import { User } from 'pixelarticons/react/User';
4
+ import React, { useEffect, useRef, useState } from 'react';
5
+ import styled from 'styled-components';
6
+ import { ConfirmModal } from '../ConfirmModal';
7
+ import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
8
+ import { CTAButton } from '../shared/CTAButton/CTAButton';
9
+
10
+ export interface IMyCharacterListingsPanelProps {
11
+ myCharacterListings: ICharacterListing[];
12
+ totalCount: number;
13
+ currentPage: number;
14
+ itemsPerPage: number;
15
+ onPageChange: (page: number) => void;
16
+ onCharacterDelist: (listingId: string) => void;
17
+ /** Items atlas — for UI sprites like the DC coin */
18
+ atlasJSON: any;
19
+ atlasIMG: any;
20
+ /** Entities atlas — for character sprites */
21
+ characterAtlasJSON: any;
22
+ characterAtlasIMG: any;
23
+ enableHotkeys?: () => void;
24
+ disableHotkeys?: () => void;
25
+ }
26
+
27
+ export const MyCharacterListingsPanel: React.FC<IMyCharacterListingsPanelProps> = ({
28
+ myCharacterListings,
29
+ totalCount,
30
+ currentPage,
31
+ itemsPerPage,
32
+ onPageChange,
33
+ onCharacterDelist,
34
+ atlasJSON,
35
+ atlasIMG,
36
+ characterAtlasJSON,
37
+ characterAtlasIMG,
38
+ enableHotkeys,
39
+ }) => {
40
+ const [delistingListingId, setDelistingListingId] = useState<string | null>(null);
41
+ const itemsContainer = useRef<HTMLDivElement>(null);
42
+
43
+ useEffect(() => {
44
+ itemsContainer.current?.scrollTo(0, 0);
45
+ }, [currentPage]);
46
+
47
+ const handleDelistClick = (listingId: string) => {
48
+ setDelistingListingId(listingId);
49
+ };
50
+
51
+ const handleDelistConfirm = () => {
52
+ if (delistingListingId) {
53
+ onCharacterDelist(delistingListingId);
54
+ setDelistingListingId(null);
55
+ enableHotkeys?.();
56
+ }
57
+ };
58
+
59
+
60
+ const getFormattedDate = (date: Date) => {
61
+ return new Date(date).toLocaleDateString();
62
+ };
63
+
64
+ return (
65
+ <>
66
+ {delistingListingId && (
67
+ <ConfirmModal
68
+ onClose={() => {
69
+ setDelistingListingId(null);
70
+ enableHotkeys?.();
71
+ }}
72
+ onConfirm={handleDelistConfirm}
73
+ message="Are you sure you want to delist this character?"
74
+ />
75
+ )}
76
+
77
+ <ListingsContainer id="MarketContainer" ref={itemsContainer}>
78
+ {myCharacterListings.length === 0 ? (
79
+ <EmptyState>
80
+ <User width={32} height={32} />
81
+ <EmptyText>You have no listed characters.</EmptyText>
82
+ <EmptySubtext>Use the "List Character" tab to list a character for sale.</EmptySubtext>
83
+ </EmptyState>
84
+ ) : (
85
+ <ListingsGrid>
86
+ {myCharacterListings.map((listing) => (
87
+ <CharacterListingCard key={listing._id}>
88
+ <CharacterSprite>
89
+ <SpriteFromAtlas
90
+ atlasIMG={characterAtlasIMG}
91
+ atlasJSON={characterAtlasJSON}
92
+ spriteKey={`${listing.characterSnapshot.textureKey}/down/standing/0.png`}
93
+ imgScale={3}
94
+ height={64}
95
+ width={64}
96
+ />
97
+ </CharacterSprite>
98
+ <CharacterInfo>
99
+ <CharacterName>{listing.characterSnapshot.name || 'Unknown'}</CharacterName>
100
+ <CharacterMeta>Level {listing.characterSnapshot.level}</CharacterMeta>
101
+ <ListingPrice>
102
+ <DCCoinWrapper>
103
+ <SpriteFromAtlas
104
+ atlasIMG={atlasIMG}
105
+ atlasJSON={atlasJSON}
106
+ spriteKey="others/definya-coin.png"
107
+ imgScale={1}
108
+ />
109
+ </DCCoinWrapper>
110
+ {formatDCAmount(listing.price)} DC
111
+ </ListingPrice>
112
+ <ListingMeta>Listed {getFormattedDate(listing.createdAt)}</ListingMeta>
113
+ {listing.isBeingBought && (
114
+ <ListingStatus $status="pending">Sale Pending</ListingStatus>
115
+ )}
116
+ </CharacterInfo>
117
+ <ActionButton
118
+ icon={<Delete width={16} height={16} />}
119
+ label="Delist"
120
+ onClick={() => handleDelistClick(listing._id)}
121
+ iconColor="#ef4444"
122
+ disabled={listing.isBeingBought}
123
+ />
124
+ </CharacterListingCard>
125
+ ))}
126
+ </ListingsGrid>
127
+ )}
128
+ </ListingsContainer>
129
+
130
+ {totalCount > itemsPerPage && (
131
+ <PagerFooter>
132
+ <Pagination>
133
+ {Array.from({ length: Math.ceil(totalCount / itemsPerPage) }, (_, i) => i + 1).map(page => (
134
+ <PageButton
135
+ key={page}
136
+ $active={currentPage === page}
137
+ type="button"
138
+ onClick={() => onPageChange(page)}
139
+ >
140
+ {page}
141
+ </PageButton>
142
+ ))}
143
+ </Pagination>
144
+ </PagerFooter>
145
+ )}
146
+ </>
147
+ );
148
+ };
149
+
150
+ const ListingsContainer = styled.div`
151
+ overflow-y: scroll;
152
+ overflow-x: hidden;
153
+ height: 390px;
154
+ width: 95%;
155
+ margin: 1rem auto 0 auto;
156
+ background: rgba(0, 0, 0, 0.2);
157
+ border: 1px solid rgba(255, 255, 255, 0.05);
158
+ border-radius: 4px;
159
+
160
+ @media (max-width: 950px) {
161
+ height: 250px;
162
+ }
163
+ `;
164
+
165
+ const ListingsGrid = styled.div`
166
+ display: grid;
167
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
168
+ gap: 12px;
169
+ padding: 12px;
170
+ `;
171
+
172
+ const CharacterListingCard = styled.div`
173
+ display: flex;
174
+ flex-direction: column;
175
+ align-items: center;
176
+ gap: 8px;
177
+ background: rgba(255, 255, 255, 0.03);
178
+ border: 1px solid rgba(255, 255, 255, 0.08);
179
+ border-radius: 8px;
180
+ padding: 12px;
181
+ `;
182
+
183
+ const CharacterSprite = styled.div`
184
+ display: flex;
185
+ align-items: center;
186
+ justify-content: center;
187
+ image-rendering: pixelated;
188
+ `;
189
+
190
+ const CharacterInfo = styled.div`
191
+ display: flex;
192
+ flex-direction: column;
193
+ align-items: center;
194
+ gap: 4px;
195
+ text-align: center;
196
+ `;
197
+
198
+ const CharacterName = styled.span`
199
+ font-family: 'Press Start 2P', cursive;
200
+ font-size: 0.55rem;
201
+ color: #f3f4f6;
202
+ text-transform: uppercase;
203
+ letter-spacing: 0.5px;
204
+ `;
205
+
206
+ const CharacterMeta = styled.span`
207
+ font-size: 0.45rem;
208
+ color: #888;
209
+ text-transform: uppercase;
210
+ letter-spacing: 0.5px;
211
+ `;
212
+
213
+ const ListingPrice = styled.div`
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 6px;
217
+ font-family: 'Press Start 2P', cursive;
218
+ font-size: 0.5rem;
219
+ color: #fef08a;
220
+ `;
221
+
222
+ const DCCoinWrapper = styled.span`
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ flex-shrink: 0;
227
+ `;
228
+
229
+ const ListingMeta = styled.span`
230
+ font-size: 0.38rem;
231
+ color: #52525b;
232
+ text-transform: uppercase;
233
+ letter-spacing: 0.5px;
234
+ `;
235
+
236
+ const ListingStatus = styled.span<{ $status: 'pending' | 'sold' }>`
237
+ font-size: 0.38rem;
238
+ padding: 2px 6px;
239
+ border-radius: 4px;
240
+ text-transform: uppercase;
241
+ letter-spacing: 0.5px;
242
+ ${({ $status }) =>
243
+ $status === 'pending'
244
+ ? `
245
+ background: rgba(239, 68, 68, 0.2);
246
+ border: 1px solid rgba(239, 68, 68, 0.4);
247
+ color: #ef4444;
248
+ `
249
+ : `
250
+ background: rgba(34, 197, 94, 0.2);
251
+ border: 1px solid rgba(34, 197, 94, 0.4);
252
+ color: #22c55e;
253
+ `}
254
+ `;
255
+
256
+ const ActionButton = styled(CTAButton)<{ disabled?: boolean }>`
257
+ padding: 6px 12px;
258
+ height: 28px;
259
+ opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
260
+ pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')};
261
+
262
+ span {
263
+ font-size: 0.5rem;
264
+ }
265
+
266
+ svg {
267
+ font-size: 1rem;
268
+ }
269
+ `;
270
+
271
+ const PagerFooter = styled.div`
272
+ display: flex;
273
+ justify-content: center;
274
+ align-items: center;
275
+ padding: 8px 0 4px;
276
+ min-height: 36px;
277
+ width: 95%;
278
+ margin: 0 auto;
279
+ `;
280
+
281
+ const Pagination = styled.div`
282
+ display: flex;
283
+ gap: 6px;
284
+ `;
285
+
286
+ const PageButton = styled.button<{ $active: boolean }>`
287
+ padding: 6px 10px;
288
+ font-family: 'Press Start 2P', cursive;
289
+ font-size: 0.5rem;
290
+ border-radius: 4px;
291
+ border: 1px solid ${({ $active }) => ($active ? '#f59e0b' : 'rgba(255,255,255,0.12)')};
292
+ background: ${({ $active }) => ($active ? 'rgba(245,158,11,0.15)' : 'rgba(0,0,0,0.3)')};
293
+ color: ${({ $active }) => ($active ? '#f59e0b' : '#777')};
294
+ cursor: pointer;
295
+ transition: border-color 0.15s, background 0.15s, color 0.15s;
296
+
297
+ &:hover {
298
+ border-color: ${({ $active }) => ($active ? '#f59e0b' : 'rgba(255,255,255,0.3)')};
299
+ color: ${({ $active }) => ($active ? '#f59e0b' : '#bbb')};
300
+ }
301
+ `;
302
+
303
+ const EmptyState = styled.div`
304
+ display: flex;
305
+ flex-direction: column;
306
+ align-items: center;
307
+ justify-content: center;
308
+ gap: 12px;
309
+ height: 100%;
310
+ min-height: 200px;
311
+ `;
312
+
313
+ const EmptyText = styled.span`
314
+ font-size: 0.55rem;
315
+ color: #71717a;
316
+ text-transform: uppercase;
317
+ letter-spacing: 1px;
318
+ `;
319
+
320
+ const EmptySubtext = styled.span`
321
+ font-size: 0.45rem;
322
+ color: #52525b;
323
+ text-transform: none;
324
+ letter-spacing: 0.5px;
325
+ text-align: center;
326
+ max-width: 280px;
327
+ `;
@@ -0,0 +1,67 @@
1
+ import { DC_TO_GOLD_SWAP_RATE } from '@rpg-engine/shared';
2
+ import React from 'react';
3
+ import styled from 'styled-components';
4
+
5
+ /** 100 DC = $1 USD */
6
+ const DC_TO_USD = 100;
7
+
8
+ export const DCRateStrip: React.FC = () => (
9
+ <RateStrip>
10
+ <RateItem>
11
+ <RateNum>1 DC</RateNum>
12
+ <RateSep>=</RateSep>
13
+ <RateVal>{DC_TO_GOLD_SWAP_RATE.toLocaleString()} Gold</RateVal>
14
+ </RateItem>
15
+ <RateDivider />
16
+ <RateItem>
17
+ <RateNum>{DC_TO_USD} DC</RateNum>
18
+ <RateSep>=</RateSep>
19
+ <RateVal>1 USD</RateVal>
20
+ </RateItem>
21
+ <RateDivider />
22
+ <RateItem>
23
+ <RateNum>1 USD</RateNum>
24
+ <RateSep>=</RateSep>
25
+ <RateVal>{(DC_TO_USD * DC_TO_GOLD_SWAP_RATE / 1000).toFixed(0)}K Gold</RateVal>
26
+ </RateItem>
27
+ </RateStrip>
28
+ );
29
+
30
+ const RateStrip = styled.div`
31
+ display: flex;
32
+ align-items: center;
33
+ border-top: 1px solid rgba(245, 158, 11, 0.1);
34
+ padding-top: 8px;
35
+ `;
36
+
37
+ const RateItem = styled.div`
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 5px;
41
+ flex: 1;
42
+ justify-content: center;
43
+ `;
44
+
45
+ const RateDivider = styled.div`
46
+ width: 1px;
47
+ height: 12px;
48
+ background: rgba(245, 158, 11, 0.2);
49
+ `;
50
+
51
+ const RateNum = styled.span`
52
+ font-family: 'Press Start 2P', cursive !important;
53
+ font-size: 6px !important;
54
+ color: rgba(255, 255, 255, 0.5) !important;
55
+ `;
56
+
57
+ const RateSep = styled.span`
58
+ font-family: 'Press Start 2P', cursive !important;
59
+ font-size: 6px !important;
60
+ color: rgba(255, 255, 255, 0.2) !important;
61
+ `;
62
+
63
+ const RateVal = styled.span`
64
+ font-family: 'Press Start 2P', cursive !important;
65
+ font-size: 6px !important;
66
+ color: rgba(254, 240, 138, 0.6) !important;
67
+ `;
@@ -14,6 +14,8 @@ export const ItemRowWrapper = styled.div<{ $isHighlighted?: boolean }>`
14
14
  transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
15
15
  position: relative;
16
16
  overflow: hidden;
17
+ width: 100%;
18
+ box-sizing: border-box;
17
19
 
18
20
  /* Subtle inner glow for premium feel */
19
21
  &::before {
@@ -30,7 +32,7 @@ export const ItemRowWrapper = styled.div<{ $isHighlighted?: boolean }>`
30
32
  border-color: ${p => p.$isHighlighted ? 'rgba(255, 215, 0, 0.6)' : 'rgba(245, 158, 11, 0.3)'};
31
33
  border-left-color: ${p => p.$isHighlighted ? '#fcd34d' : '#f59e0b'};
32
34
  box-shadow: inset 0 0 10px rgba(0,0,0,0.5), 0 4px 16px rgba(0, 0, 0, 0.4);
33
- transform: scale(1.01) translateY(-1px);
35
+ transform: translateY(-1px);
34
36
  z-index: 10;
35
37
  }
36
38
  `;