@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
@@ -0,0 +1,8 @@
1
+ import { Meta, Story } from '@storybook/react';
2
+ import { ICharacterListingModalProps } from '../../../components/Marketplace/CharacterListingModal';
3
+ declare const meta: Meta<ICharacterListingModalProps>;
4
+ export default meta;
5
+ export declare const WithMultipleCharacters: Story;
6
+ export declare const WithAlreadyListedFiltered: Story;
7
+ export declare const NoEligibleCharacters: Story;
8
+ export declare const SingleCharacter: Story;
@@ -0,0 +1,10 @@
1
+ import { Meta, Story } from '@storybook/react';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ export declare const CharacterBrowse: Story;
5
+ export declare const CharacterBrowseLoading: Story;
6
+ export declare const CharacterBrowseEmpty: Story;
7
+ export declare const CharacterBrowseFiltered: Story;
8
+ export declare const MyCharacterListings: Story;
9
+ export declare const MyCharacterListingsEmpty: Story;
10
+ export declare const CharacterListingPending: Story;
@@ -0,0 +1,8 @@
1
+ import { Meta, Story } from '@storybook/react';
2
+ import { IRadioOptionProps } from '../../components/shared/RadioOption';
3
+ declare const meta: Meta;
4
+ export default meta;
5
+ export declare const Unselected: Story<IRadioOptionProps>;
6
+ export declare const Selected: Story<IRadioOptionProps>;
7
+ export declare const Disabled: Story<IRadioOptionProps>;
8
+ export declare const InteractiveGroup: Story;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.181",
3
+ "version": "0.8.184",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { FaShoppingCart } from 'react-icons/fa';
3
3
  import styled from 'styled-components';
4
4
  import { InternalTabs } from '../InternalTabs/InternalTabs';
5
+ import { DCRateStrip } from '../shared/DCRateStrip';
5
6
  import { DCHistoryPanel, IDCTransaction } from './DCHistoryPanel';
6
7
  import { DCTransferPanel, IDCTransferCharacterResult } from './DCTransferPanel';
7
8
 
@@ -22,7 +23,8 @@ export interface IDCWalletContentProps {
22
23
  searchResults?: IDCTransferCharacterResult[];
23
24
  }
24
25
 
25
- const DC_TO_GOLD = 5500;
26
+ import { DC_TO_GOLD_SWAP_RATE } from '@rpg-engine/shared';
27
+
26
28
  const DC_TO_USD = 100;
27
29
 
28
30
  export const DCWalletContent: React.FC<IDCWalletContentProps> = ({
@@ -42,7 +44,7 @@ export const DCWalletContent: React.FC<IDCWalletContentProps> = ({
42
44
  searchResults,
43
45
  }) => {
44
46
  const usdValue = (dcBalance / DC_TO_USD).toFixed(2);
45
- const goldValue = (dcBalance * DC_TO_GOLD).toLocaleString();
47
+ const goldValue = (dcBalance * DC_TO_GOLD_SWAP_RATE).toLocaleString();
46
48
 
47
49
  const tabs = [
48
50
  {
@@ -94,13 +96,7 @@ export const DCWalletContent: React.FC<IDCWalletContentProps> = ({
94
96
  </BuyButton>
95
97
  )}
96
98
  </BalanceTop>
97
- <RateStrip>
98
- <RateItem><RateNum>1 DC</RateNum><RateSep>=</RateSep><RateVal>5,500 Gold</RateVal></RateItem>
99
- <RateDivider />
100
- <RateItem><RateNum>100 DC</RateNum><RateSep>=</RateSep><RateVal>1 USD</RateVal></RateItem>
101
- <RateDivider />
102
- <RateItem><RateNum>1 USD</RateNum><RateSep>=</RateSep><RateVal>550K Gold</RateVal></RateItem>
103
- </RateStrip>
99
+ <DCRateStrip />
104
100
  </BalanceHeader>
105
101
 
106
102
  <TabsWrapper>
@@ -207,44 +203,6 @@ const BuyButtonLabel = styled.span`
207
203
  text-shadow: none !important;
208
204
  `;
209
205
 
210
- const RateStrip = styled.div`
211
- display: flex;
212
- align-items: center;
213
- border-top: 1px solid rgba(245, 158, 11, 0.1);
214
- padding-top: 8px;
215
- `;
216
-
217
- const RateItem = styled.div`
218
- display: flex;
219
- align-items: center;
220
- gap: 5px;
221
- flex: 1;
222
- justify-content: center;
223
- `;
224
-
225
- const RateDivider = styled.div`
226
- width: 1px;
227
- height: 12px;
228
- background: rgba(245, 158, 11, 0.2);
229
- `;
230
-
231
- const RateNum = styled.span`
232
- font-family: 'Press Start 2P', cursive !important;
233
- font-size: 6px !important;
234
- color: rgba(255, 255, 255, 0.5) !important;
235
- `;
236
-
237
- const RateSep = styled.span`
238
- font-family: 'Press Start 2P', cursive !important;
239
- font-size: 6px !important;
240
- color: rgba(255, 255, 255, 0.2) !important;
241
- `;
242
-
243
- const RateVal = styled.span`
244
- font-family: 'Press Start 2P', cursive !important;
245
- font-size: 6px !important;
246
- color: rgba(254, 240, 138, 0.6) !important;
247
- `;
248
206
 
249
207
  const TabsWrapper = styled.div`
250
208
  padding: 0 2.5%;
@@ -643,6 +643,7 @@ const WrapperContainer = styled.div<{ $sell: boolean }>`
643
643
 
644
644
  const ItemComponentScrollWrapper = styled.div`
645
645
  overflow-y: scroll;
646
+ overflow-x: hidden;
646
647
  height: 390px;
647
648
  width: 95%;
648
649
  margin: 1rem auto 0 auto;
@@ -0,0 +1,102 @@
1
+ import { ICharacter } from '@rpg-engine/shared';
2
+ import { ShoppingBag } from 'pixelarticons/react/ShoppingBag';
3
+ import React, { useState } from 'react';
4
+ import styled from 'styled-components';
5
+ import { CTAButton } from '../shared/CTAButton/CTAButton';
6
+ import { CharacterListingModal } from './CharacterListingModal';
7
+
8
+ export interface ICharacterListingFormProps {
9
+ accountCharacters: ICharacter[];
10
+ onCharacterList: (characterId: string, price: number) => void;
11
+ /** Items atlas — for UI sprites like the DC coin */
12
+ atlasJSON: any;
13
+ atlasIMG: any;
14
+ /** Entities atlas — for character sprites */
15
+ characterAtlasJSON: any;
16
+ characterAtlasIMG: any;
17
+ enableHotkeys?: () => void;
18
+ disableHotkeys?: () => void;
19
+ }
20
+
21
+ export const CharacterListingForm: React.FC<ICharacterListingFormProps> = ({
22
+ accountCharacters,
23
+ onCharacterList,
24
+ atlasJSON,
25
+ atlasIMG,
26
+ characterAtlasJSON,
27
+ characterAtlasIMG,
28
+ enableHotkeys,
29
+ disableHotkeys,
30
+ }) => {
31
+ const [isModalOpen, setIsModalOpen] = useState(false);
32
+
33
+ const eligibleCount = accountCharacters.filter(
34
+ c => !c.isListedForSale && !c.tradedAt
35
+ ).length;
36
+
37
+ return (
38
+ <Wrapper>
39
+ <CharacterListingModal
40
+ isOpen={isModalOpen}
41
+ onClose={() => setIsModalOpen(false)}
42
+ accountCharacters={accountCharacters}
43
+ atlasJSON={atlasJSON}
44
+ atlasIMG={atlasIMG}
45
+ characterAtlasJSON={characterAtlasJSON}
46
+ characterAtlasIMG={characterAtlasIMG}
47
+ onCharacterList={onCharacterList}
48
+ enableHotkeys={enableHotkeys}
49
+ disableHotkeys={disableHotkeys}
50
+ />
51
+
52
+ <Description>
53
+ List one of your offline characters on the marketplace. Prices are set in DC.
54
+ </Description>
55
+
56
+ <OpenButton
57
+ icon={<ShoppingBag width={20} height={20} />}
58
+ label={`List a Character${eligibleCount > 0 ? ` (${eligibleCount} eligible)` : ''}`}
59
+ disabled={eligibleCount === 0}
60
+ onClick={() => setIsModalOpen(true)}
61
+ />
62
+
63
+ {eligibleCount === 0 && (
64
+ <NoEligible>No eligible characters to list.</NoEligible>
65
+ )}
66
+ </Wrapper>
67
+ );
68
+ };
69
+
70
+ const Wrapper = styled.div`
71
+ display: flex;
72
+ flex-direction: column;
73
+ align-items: center;
74
+ justify-content: center;
75
+ gap: 20px;
76
+ padding: 40px 24px;
77
+ width: 95%;
78
+ margin: 0 auto;
79
+ `;
80
+
81
+ const Description = styled.p`
82
+ margin: 0;
83
+ font-size: 0.5rem;
84
+ color: #666;
85
+ text-align: center;
86
+ line-height: 1.6;
87
+ max-width: 320px;
88
+ `;
89
+
90
+ const OpenButton = styled(CTAButton)`
91
+ padding: 14px 24px;
92
+
93
+ span { font-size: 0.65rem; }
94
+ svg { font-size: 1.2rem; }
95
+ `;
96
+
97
+ const NoEligible = styled.span`
98
+ font-size: 0.45rem;
99
+ color: #52525b;
100
+ text-transform: uppercase;
101
+ letter-spacing: 1px;
102
+ `;
@@ -0,0 +1,404 @@
1
+ import { ICharacter } from '@rpg-engine/shared';
2
+ import { ShoppingBag } from 'pixelarticons/react/ShoppingBag';
3
+ import React, { useCallback, useState } from 'react';
4
+ import { FaTimes } from 'react-icons/fa';
5
+ import styled, { keyframes } from 'styled-components';
6
+ import ModalPortal from '../Abstractions/ModalPortal';
7
+ import { ConfirmModal } from '../ConfirmModal';
8
+ import { Input } from '../Input';
9
+ import { CTAButton } from '../shared/CTAButton/CTAButton';
10
+ import { DCRateStrip } from '../shared/DCRateStrip';
11
+ import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
12
+
13
+ export interface ICharacterListingModalProps {
14
+ isOpen: boolean;
15
+ onClose: () => void;
16
+ accountCharacters: ICharacter[];
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
+ onCharacterList: (characterId: string, price: number) => void;
24
+ enableHotkeys?: () => void;
25
+ disableHotkeys?: () => void;
26
+ }
27
+
28
+ export const CharacterListingModal: React.FC<ICharacterListingModalProps> = ({
29
+ isOpen,
30
+ onClose,
31
+ accountCharacters,
32
+ atlasJSON,
33
+ atlasIMG,
34
+ characterAtlasJSON,
35
+ characterAtlasIMG,
36
+ onCharacterList,
37
+ enableHotkeys,
38
+ disableHotkeys,
39
+ }) => {
40
+ const [selectedId, setSelectedId] = useState<string | null>(null);
41
+ const [price, setPrice] = useState('');
42
+ const [isConfirming, setIsConfirming] = useState(false);
43
+
44
+ if (!isOpen) return null;
45
+
46
+ const eligibleCharacters = accountCharacters.filter(
47
+ c => !c.isListedForSale && !c.tradedAt
48
+ );
49
+
50
+ const canList = !!selectedId && Number(price) > 0;
51
+
52
+ const handleClose = () => {
53
+ setSelectedId(null);
54
+ setPrice('');
55
+ enableHotkeys?.();
56
+ onClose();
57
+ };
58
+
59
+ const handleListClick = () => {
60
+ if (canList) setIsConfirming(true);
61
+ };
62
+
63
+ const handleConfirm = () => {
64
+ if (!selectedId || !canList) return;
65
+ onCharacterList(selectedId, Number(price));
66
+ setSelectedId(null);
67
+ setPrice('');
68
+ setIsConfirming(false);
69
+ enableHotkeys?.();
70
+ onClose();
71
+ };
72
+
73
+ const stopPropagation = useCallback(
74
+ (e: React.MouseEvent | React.TouchEvent | React.PointerEvent) => {
75
+ e.stopPropagation();
76
+ },
77
+ []
78
+ );
79
+
80
+ const getLevel = (c: ICharacter) => c.skills?.level || 1;
81
+
82
+ return (
83
+ <ModalPortal>
84
+ {isConfirming && (
85
+ <ConfirmModal
86
+ onConfirm={handleConfirm}
87
+ onClose={() => { setIsConfirming(false); enableHotkeys?.(); }}
88
+ message="Are you sure you want to list this character for sale?"
89
+ />
90
+ )}
91
+
92
+ <Overlay onPointerDown={handleClose} />
93
+ <ModalContainer>
94
+ <ModalContent
95
+ onClick={stopPropagation as React.MouseEventHandler}
96
+ onTouchStart={stopPropagation as React.TouchEventHandler}
97
+ onPointerDown={stopPropagation as React.PointerEventHandler}
98
+ >
99
+ <Header>
100
+ <Title>List Character for Sale</Title>
101
+ <CloseButton onPointerDown={handleClose} aria-label="Close" type="button">
102
+ <FaTimes />
103
+ </CloseButton>
104
+ </Header>
105
+
106
+ <CharacterList>
107
+ {eligibleCharacters.length === 0 ? (
108
+ <EmptyState>No eligible characters to list.</EmptyState>
109
+ ) : (
110
+ eligibleCharacters.map(character => {
111
+ const isSelected = selectedId === character._id;
112
+ return (
113
+ <CharacterRow
114
+ key={character._id}
115
+ $selected={isSelected}
116
+ onPointerDown={() => setSelectedId(character._id!)}
117
+ role="radio"
118
+ aria-checked={isSelected}
119
+ tabIndex={0}
120
+ onKeyDown={e => {
121
+ if (e.key === 'Enter' || e.key === ' ') {
122
+ e.preventDefault();
123
+ setSelectedId(character._id!);
124
+ }
125
+ }}
126
+ >
127
+ <RadioCircle $selected={isSelected} />
128
+ <SpriteWrapper>
129
+ <SpriteFromAtlas
130
+ atlasIMG={characterAtlasIMG}
131
+ atlasJSON={characterAtlasJSON}
132
+ spriteKey={`${character.textureKey}/down/standing/0.png`}
133
+ imgScale={2}
134
+ height={40}
135
+ width={40}
136
+ />
137
+ </SpriteWrapper>
138
+ <CharacterInfo>
139
+ <CharacterName>{character.name || 'Unknown'}</CharacterName>
140
+ <CharacterMeta>Level {getLevel(character)}</CharacterMeta>
141
+ </CharacterInfo>
142
+ </CharacterRow>
143
+ );
144
+ })
145
+ )}
146
+ </CharacterList>
147
+
148
+ <PriceSection>
149
+ <PriceLabel>Set Price (DC)</PriceLabel>
150
+ <PriceRow>
151
+ <Input
152
+ onChange={e => setPrice(e.target.value)}
153
+ value={price}
154
+ placeholder="Amount..."
155
+ type="number"
156
+ disabled={eligibleCharacters.length === 0}
157
+ onFocus={disableHotkeys}
158
+ onBlur={enableHotkeys}
159
+ className="price-input"
160
+ />
161
+ <ListBtn
162
+ icon={<ShoppingBag width={18} height={18} />}
163
+ label="List"
164
+ disabled={!canList}
165
+ onClick={handleListClick}
166
+ />
167
+ </PriceRow>
168
+ {price && Number(price) > 0 && (
169
+ <PricePreview>
170
+ <DCCoinWrapper>
171
+ <SpriteFromAtlas
172
+ atlasIMG={atlasIMG}
173
+ atlasJSON={atlasJSON}
174
+ spriteKey="others/definya-coin.png"
175
+ imgScale={1}
176
+ />
177
+ </DCCoinWrapper>
178
+ <PricePreviewAmount>{Number(price).toLocaleString()} DC</PricePreviewAmount>
179
+ </PricePreview>
180
+ )}
181
+ <DCRateStrip />
182
+ </PriceSection>
183
+ </ModalContent>
184
+ </ModalContainer>
185
+ </ModalPortal>
186
+ );
187
+ };
188
+
189
+ const scaleIn = keyframes`
190
+ from { transform: scale(0.85); opacity: 0; }
191
+ to { transform: scale(1); opacity: 1; }
192
+ `;
193
+
194
+ const Overlay = styled.div`
195
+ position: fixed;
196
+ inset: 0;
197
+ background: rgba(0, 0, 0, 0.7);
198
+ z-index: 1000;
199
+ `;
200
+
201
+ const ModalContainer = styled.div`
202
+ position: fixed;
203
+ inset: 0;
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ z-index: 1001;
208
+ pointer-events: none;
209
+ `;
210
+
211
+ const ModalContent = styled.div`
212
+ background: #1a1a2e;
213
+ border: 2px solid #f59e0b;
214
+ border-radius: 8px;
215
+ padding: 20px 24px;
216
+ width: 440px;
217
+ max-width: 92%;
218
+ max-height: 80dvh;
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: 16px;
222
+ overflow: hidden;
223
+ pointer-events: auto;
224
+ animation: ${scaleIn} 0.15s ease-out;
225
+ `;
226
+
227
+ const Header = styled.div`
228
+ display: flex;
229
+ align-items: center;
230
+ justify-content: space-between;
231
+ `;
232
+
233
+ const Title = styled.h3`
234
+ margin: 0;
235
+ font-family: 'Press Start 2P', cursive !important;
236
+ font-size: 0.65rem !important;
237
+ color: #fef08a !important;
238
+ `;
239
+
240
+ const CloseButton = styled.button`
241
+ background: none;
242
+ border: none;
243
+ color: rgba(255, 255, 255, 0.6);
244
+ cursor: pointer;
245
+ font-size: 1rem;
246
+ padding: 4px;
247
+ display: flex;
248
+ align-items: center;
249
+ pointer-events: auto;
250
+
251
+ &:hover { color: #ffffff; }
252
+ `;
253
+
254
+ const CharacterList = styled.div`
255
+ display: flex;
256
+ flex-direction: column;
257
+ gap: 6px;
258
+ overflow-y: auto;
259
+ max-height: 260px;
260
+ padding: 2px;
261
+
262
+ &::-webkit-scrollbar { width: 6px; }
263
+ &::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 4px; }
264
+ &::-webkit-scrollbar-thumb { background: rgba(245,158,11,0.3); border-radius: 4px; }
265
+ `;
266
+
267
+ const CharacterRow = styled.div<{ $selected: boolean }>`
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 12px;
271
+ padding: 10px 12px;
272
+ border: 1px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.08)')};
273
+ border-radius: 6px;
274
+ background: ${({ $selected }) => ($selected ? 'rgba(245,158,11,0.1)' : 'rgba(255,255,255,0.02)')};
275
+ cursor: pointer;
276
+ transition: border-color 0.15s, background 0.15s;
277
+
278
+ &:hover {
279
+ border-color: ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(245,158,11,0.4)')};
280
+ background: ${({ $selected }) => ($selected ? 'rgba(245,158,11,0.1)' : 'rgba(245,158,11,0.05)')};
281
+ }
282
+
283
+ &:focus-visible {
284
+ outline: 2px solid rgba(245,158,11,0.6);
285
+ outline-offset: 2px;
286
+ }
287
+ `;
288
+
289
+ const RadioCircle = styled.div<{ $selected: boolean }>`
290
+ width: 14px;
291
+ height: 14px;
292
+ border-radius: 50%;
293
+ border: 2px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.4)')};
294
+ flex-shrink: 0;
295
+ display: flex;
296
+ align-items: center;
297
+ justify-content: center;
298
+
299
+ &::after {
300
+ content: '';
301
+ width: 6px;
302
+ height: 6px;
303
+ border-radius: 50%;
304
+ background: #f59e0b;
305
+ opacity: ${({ $selected }) => ($selected ? 1 : 0)};
306
+ transition: opacity 0.15s;
307
+ }
308
+ `;
309
+
310
+ const SpriteWrapper = styled.div`
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: center;
314
+ image-rendering: pixelated;
315
+ flex-shrink: 0;
316
+ width: 40px;
317
+ height: 40px;
318
+ `;
319
+
320
+ const CharacterInfo = styled.div`
321
+ display: flex;
322
+ flex-direction: column;
323
+ gap: 4px;
324
+ flex: 1;
325
+ `;
326
+
327
+ const CharacterName = styled.span`
328
+ font-family: 'Press Start 2P', cursive !important;
329
+ font-size: 0.5rem !important;
330
+ color: #f3f4f6 !important;
331
+ `;
332
+
333
+ const CharacterMeta = styled.span`
334
+ font-family: 'Press Start 2P', cursive !important;
335
+ font-size: 0.4rem !important;
336
+ color: #888 !important;
337
+ text-transform: uppercase;
338
+ letter-spacing: 0.5px;
339
+ `;
340
+
341
+ const PricePreview = styled.div`
342
+ display: flex;
343
+ align-items: center;
344
+ gap: 6px;
345
+ `;
346
+
347
+ const DCCoinWrapper = styled.span`
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: center;
351
+ flex-shrink: 0;
352
+ `;
353
+
354
+ const PricePreviewAmount = styled.span`
355
+ font-family: 'Press Start 2P', cursive !important;
356
+ font-size: 0.55rem !important;
357
+ color: #fef08a !important;
358
+ `;
359
+
360
+ const PriceSection = styled.div`
361
+ display: flex;
362
+ flex-direction: column;
363
+ gap: 8px;
364
+ border-top: 1px solid rgba(255,255,255,0.06);
365
+ padding-top: 12px;
366
+ `;
367
+
368
+ const PriceLabel = styled.p`
369
+ margin: 0;
370
+ font-family: 'Press Start 2P', cursive !important;
371
+ font-size: 0.55rem !important;
372
+ color: #888 !important;
373
+ text-transform: uppercase;
374
+ letter-spacing: 0.5px;
375
+ `;
376
+
377
+ const PriceRow = styled.div`
378
+ display: flex;
379
+ gap: 8px;
380
+ align-items: center;
381
+
382
+ .price-input { flex: 1; height: 12px; }
383
+ `;
384
+
385
+ const ListBtn = styled(CTAButton)`
386
+ flex-shrink: 0;
387
+ padding: 10px 16px;
388
+ height: 32px;
389
+
390
+ span { font-size: 0.6rem; }
391
+ svg { font-size: 1.1rem; }
392
+ `;
393
+
394
+ const EmptyState = styled.div`
395
+ display: flex;
396
+ align-items: center;
397
+ justify-content: center;
398
+ padding: 32px 16px;
399
+ font-family: 'Press Start 2P', cursive !important;
400
+ font-size: 0.5rem !important;
401
+ color: #666 !important;
402
+ text-transform: uppercase;
403
+ letter-spacing: 1px;
404
+ `;