@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,93 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ export interface IRadioOptionProps {
5
+ selected: boolean;
6
+ disabled?: boolean;
7
+ onSelect: () => void;
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ /**
12
+ * A selectable row with an amber radio circle indicator.
13
+ * Used for single-select option lists throughout the Marketplace UI.
14
+ * Export `RadioCircle` separately so consumers can compose custom layouts.
15
+ */
16
+ export const RadioOption: React.FC<IRadioOptionProps> = ({
17
+ selected,
18
+ disabled = false,
19
+ onSelect,
20
+ children,
21
+ }) => {
22
+ const handleClick = () => {
23
+ if (!disabled) {
24
+ onSelect();
25
+ }
26
+ };
27
+
28
+ return (
29
+ <RadioOptionContainer
30
+ $selected={selected}
31
+ $disabled={disabled}
32
+ onClick={handleClick}
33
+ role="radio"
34
+ aria-checked={selected}
35
+ aria-disabled={disabled}
36
+ >
37
+ <RadioCircle $selected={selected} />
38
+ {children}
39
+ </RadioOptionContainer>
40
+ );
41
+ };
42
+
43
+ const RadioOptionContainer = styled.div<{ $selected: boolean; $disabled?: boolean }>`
44
+ display: flex;
45
+ align-items: center;
46
+ gap: 12px;
47
+ padding: 10px 12px;
48
+ border: 1px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.15)')};
49
+ border-radius: 6px;
50
+ background: ${({ $selected }) => ($selected ? 'rgba(245,158,11,0.1)' : 'transparent')};
51
+ cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
52
+ opacity: ${({ $disabled }) => ($disabled ? 0.5 : 1)};
53
+ transition: border-color 0.15s, background 0.15s;
54
+
55
+ &:hover {
56
+ border-color: ${({ $disabled }) => ($disabled ? 'rgba(255,255,255,0.15)' : '#f59e0b')};
57
+ }
58
+ `;
59
+
60
+ export const RadioCircle = styled.div<{ $selected: boolean }>`
61
+ width: 16px;
62
+ height: 16px;
63
+ border-radius: 50%;
64
+ border: 2px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.4)')};
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ flex-shrink: 0;
69
+
70
+ &::after {
71
+ content: '';
72
+ width: 8px;
73
+ height: 8px;
74
+ border-radius: 50%;
75
+ background: #f59e0b;
76
+ opacity: ${({ $selected }) => ($selected ? 1 : 0)};
77
+ transition: opacity 0.15s;
78
+ }
79
+ `;
80
+
81
+ /** Convenience wrapper for option label text with RPGUI font override. */
82
+ export const RadioOptionLabel = styled.span<{ $disabled?: boolean }>`
83
+ font-family: 'Press Start 2P', cursive !important;
84
+ font-size: 0.65rem !important;
85
+ color: ${({ $disabled }) => ($disabled ? 'rgba(255,255,255,0.4)' : '#ffffff')} !important;
86
+ `;
87
+
88
+ /** Convenience wrapper for option sub-text with RPGUI font override. */
89
+ export const RadioOptionSub = styled.span`
90
+ font-family: 'Press Start 2P', cursive !important;
91
+ font-size: 0.55rem !important;
92
+ color: rgba(255, 255, 255, 0.5) !important;
93
+ `;
package/src/index.tsx CHANGED
@@ -43,6 +43,10 @@ export * from './components/Marketplace/BuyOrderPanel';
43
43
  export * from './components/Marketplace/BuyOrderRows';
44
44
  export * from './components/Marketplace/HistoryPanel';
45
45
  export * from './components/Marketplace/BlueprintSearchModal';
46
+ export * from './components/Marketplace/CharacterMarketplacePanel';
47
+ export * from './components/Marketplace/CharacterMarketplaceRows';
48
+ export * from './components/Marketplace/CharacterListingForm';
49
+ export * from './components/Marketplace/MyCharacterListingsPanel';
46
50
  export * from './components/Multitab/TabBody';
47
51
  export * from './components/Multitab/TabsContainer';
48
52
  export * from './components/NPCDialog/NPCDialog';
@@ -0,0 +1,131 @@
1
+ import { ICharacter } from '@rpg-engine/shared';
2
+ import { Meta, Story } from '@storybook/react';
3
+ import React, { useState } from 'react';
4
+ import { CharacterListingModal, ICharacterListingModalProps } from '../../../components/Marketplace/CharacterListingModal';
5
+ import { RPGUIRoot } from '../../..';
6
+ import entitiesAtlasJSON from '../../../mocks/atlas/entities/entities.json';
7
+ import entitiesAtlasIMG from '../../../mocks/atlas/entities/entities.png';
8
+ import itemsAtlasJSON from '../../../mocks/atlas/items/items.json';
9
+ import itemsAtlasIMG from '../../../mocks/atlas/items/items.png';
10
+
11
+ const meta: Meta<ICharacterListingModalProps> = {
12
+ title: 'Features/Marketplace/CharacterListingModal',
13
+ component: CharacterListingModal,
14
+ };
15
+
16
+ export default meta;
17
+
18
+ const mockCharacters: ICharacter[] = [
19
+ {
20
+ _id: 'char-1',
21
+ name: 'Sir Galahad',
22
+ textureKey: 'black-knight',
23
+ isListedForSale: false,
24
+ tradedAt: undefined,
25
+ skills: { level: 25 } as any,
26
+ } as ICharacter,
27
+ {
28
+ _id: 'char-2',
29
+ name: 'Merlin Jr.',
30
+ textureKey: 'pink-mage-1',
31
+ isListedForSale: false,
32
+ tradedAt: undefined,
33
+ skills: { level: 30 } as any,
34
+ } as ICharacter,
35
+ {
36
+ _id: 'char-3',
37
+ name: 'ShadowStep',
38
+ textureKey: 'redhair-girl-1',
39
+ isListedForSale: false,
40
+ tradedAt: undefined,
41
+ skills: { level: 18 } as any,
42
+ } as ICharacter,
43
+ ];
44
+
45
+ const mockCharactersWithListed: ICharacter[] = [
46
+ ...mockCharacters,
47
+ {
48
+ _id: 'char-4',
49
+ name: 'AlreadyListed',
50
+ textureKey: 'dragon-knight',
51
+ isListedForSale: true,
52
+ tradedAt: undefined,
53
+ skills: { level: 40 } as any,
54
+ } as ICharacter,
55
+ ];
56
+
57
+ const ModalWrapper: React.FC<Omit<ICharacterListingModalProps, 'isOpen' | 'onClose' | 'onCharacterList'>> = (props) => {
58
+ const [isOpen, setIsOpen] = useState(true);
59
+ const [lastListed, setLastListed] = useState<string | null>(null);
60
+
61
+ return (
62
+ <RPGUIRoot>
63
+ <div style={{ padding: '20px', fontFamily: 'monospace', fontSize: '12px', color: '#fff' }}>
64
+ {lastListed ? (
65
+ <p>✅ Listed: {lastListed}</p>
66
+ ) : (
67
+ <p>Click "List" to submit the form</p>
68
+ )}
69
+ <button
70
+ style={{ marginTop: 8, padding: '6px 12px', cursor: 'pointer' }}
71
+ onClick={() => setIsOpen(true)}
72
+ >
73
+ Reopen Modal
74
+ </button>
75
+ </div>
76
+ <CharacterListingModal
77
+ {...props}
78
+ isOpen={isOpen}
79
+ onClose={() => setIsOpen(false)}
80
+ onCharacterList={(id, price) => {
81
+ setLastListed(`ID=${id}, Price=${price} DC`);
82
+ setIsOpen(false);
83
+ }}
84
+ />
85
+ </RPGUIRoot>
86
+ );
87
+ };
88
+
89
+ export const WithMultipleCharacters: Story = () => (
90
+ <ModalWrapper
91
+ accountCharacters={mockCharacters}
92
+ atlasJSON={itemsAtlasJSON}
93
+ atlasIMG={itemsAtlasIMG}
94
+ characterAtlasJSON={entitiesAtlasJSON}
95
+ characterAtlasIMG={entitiesAtlasIMG}
96
+ />
97
+ );
98
+ WithMultipleCharacters.storyName = 'Multiple eligible characters';
99
+
100
+ export const WithAlreadyListedFiltered: Story = () => (
101
+ <ModalWrapper
102
+ accountCharacters={mockCharactersWithListed}
103
+ atlasJSON={itemsAtlasJSON}
104
+ atlasIMG={itemsAtlasIMG}
105
+ characterAtlasJSON={entitiesAtlasJSON}
106
+ characterAtlasIMG={entitiesAtlasIMG}
107
+ />
108
+ );
109
+ WithAlreadyListedFiltered.storyName = 'Some characters already listed (filtered out)';
110
+
111
+ export const NoEligibleCharacters: Story = () => (
112
+ <ModalWrapper
113
+ accountCharacters={[]}
114
+ atlasJSON={itemsAtlasJSON}
115
+ atlasIMG={itemsAtlasIMG}
116
+ characterAtlasJSON={entitiesAtlasJSON}
117
+ characterAtlasIMG={entitiesAtlasIMG}
118
+ />
119
+ );
120
+ NoEligibleCharacters.storyName = 'No eligible characters';
121
+
122
+ export const SingleCharacter: Story = () => (
123
+ <ModalWrapper
124
+ accountCharacters={[mockCharacters[0]]}
125
+ atlasJSON={itemsAtlasJSON}
126
+ atlasIMG={itemsAtlasIMG}
127
+ characterAtlasJSON={entitiesAtlasJSON}
128
+ characterAtlasIMG={entitiesAtlasIMG}
129
+ />
130
+ );
131
+ SingleCharacter.storyName = 'Single eligible character';
@@ -0,0 +1,340 @@
1
+ import { ICharacterListing } from '@rpg-engine/shared';
2
+ import { Meta, Story } from '@storybook/react';
3
+ import React from 'react';
4
+ import { RPGUIRoot } from '../../..';
5
+ import { CharacterMarketplacePanel } from '../../../components/Marketplace/CharacterMarketplacePanel';
6
+ import { MyCharacterListingsPanel } from '../../../components/Marketplace/MyCharacterListingsPanel';
7
+ import entitiesAtlasJSON from '../../../mocks/atlas/entities/entities.json';
8
+ import entitiesAtlasIMG from '../../../mocks/atlas/entities/entities.png';
9
+ import itemsAtlasJSON from '../../../mocks/atlas/items/items.json';
10
+ import itemsAtlasIMG from '../../../mocks/atlas/items/items.png';
11
+
12
+ const meta: Meta = {
13
+ title: 'Features/Marketplace/CharacterMarketplace',
14
+ component: CharacterMarketplacePanel,
15
+ };
16
+
17
+ export default meta;
18
+
19
+ const now = new Date();
20
+ const daysAgo = (days: number): Date =>
21
+ new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
22
+
23
+ const mockCharacterListings: ICharacterListing[] = [
24
+ {
25
+ _id: 'cl-1',
26
+ character: 'char-1',
27
+ seller: 'player-2',
28
+ listedByCharacterName: 'WarriorKing',
29
+ price: 50000,
30
+ isBeingBought: false,
31
+ characterSnapshot: {
32
+ name: 'Sir Galahad',
33
+ level: 25,
34
+ class: 'Warrior',
35
+ race: 'Human',
36
+ faction: 'Alliance',
37
+ mode: 'Hardcore',
38
+ skills: { sword: 10, shield: 8 },
39
+ equipment: [
40
+ { slot: 'weapon', itemName: 'Broad Sword', itemKey: 'items/broad-sword', rarity: 'Common' },
41
+ { slot: 'armor', itemName: 'Steel Armor', itemKey: 'items/steel-armor', rarity: 'Rare' },
42
+ ],
43
+ textureKey: 'black-knight',
44
+ },
45
+ createdAt: daysAgo(3),
46
+ updatedAt: daysAgo(3),
47
+ },
48
+ {
49
+ _id: 'cl-2',
50
+ character: 'char-2',
51
+ seller: 'player-3',
52
+ listedByCharacterName: 'MageLord',
53
+ price: 75000,
54
+ isBeingBought: false,
55
+ characterSnapshot: {
56
+ name: 'Merlin Jr.',
57
+ level: 30,
58
+ class: 'Mage',
59
+ race: 'Elf',
60
+ faction: 'Horde',
61
+ mode: 'Standard',
62
+ skills: { fireball: 15, frostbolt: 12 },
63
+ equipment: [
64
+ { slot: 'weapon', itemName: 'Fire Staff', itemKey: 'items/fire-staff', rarity: 'Epic' },
65
+ ],
66
+ textureKey: 'pink-mage-1',
67
+ },
68
+ createdAt: daysAgo(1),
69
+ updatedAt: daysAgo(1),
70
+ },
71
+ {
72
+ _id: 'cl-3',
73
+ character: 'char-3',
74
+ seller: 'player-4',
75
+ listedByCharacterName: 'RogueShadow',
76
+ price: 40000,
77
+ isBeingBought: true,
78
+ characterSnapshot: {
79
+ name: 'ShadowStep',
80
+ level: 20,
81
+ class: 'Rogue',
82
+ race: 'Dark Elf',
83
+ faction: 'Horde',
84
+ mode: 'Hardcore',
85
+ skills: { stealth: 10, backstab: 12 },
86
+ equipment: [
87
+ { slot: 'weapon', itemName: 'Dagger', itemKey: 'items/dagger', rarity: 'Uncommon' },
88
+ ],
89
+ textureKey: 'redhair-girl-1',
90
+ },
91
+ createdAt: daysAgo(7),
92
+ updatedAt: daysAgo(1),
93
+ },
94
+ {
95
+ _id: 'cl-4',
96
+ character: 'char-4',
97
+ seller: 'player-5',
98
+ listedByCharacterName: 'PaladinLight',
99
+ price: 100000,
100
+ isBeingBought: false,
101
+ characterSnapshot: {
102
+ name: 'Lightbringer',
103
+ level: 40,
104
+ class: 'Paladin',
105
+ race: 'Human',
106
+ faction: 'Alliance',
107
+ mode: 'Standard',
108
+ skills: { holy: 20, shield: 15 },
109
+ equipment: [
110
+ { slot: 'weapon', itemName: 'Holy Sword', itemKey: 'items/holy-sword', rarity: 'Legendary' },
111
+ { slot: 'armor', itemName: 'Divine Armor', itemKey: 'items/divine-armor', rarity: 'Epic' },
112
+ ],
113
+ textureKey: 'dragon-knight',
114
+ },
115
+ createdAt: daysAgo(14),
116
+ updatedAt: daysAgo(14),
117
+ },
118
+ {
119
+ _id: 'cl-5',
120
+ character: 'char-5',
121
+ seller: 'player-6',
122
+ listedByCharacterName: 'ArcherQueen',
123
+ price: 35000,
124
+ isBeingBought: false,
125
+ characterSnapshot: {
126
+ name: 'ArrowStorm',
127
+ level: 18,
128
+ class: 'Archer',
129
+ race: 'Wood Elf',
130
+ faction: 'Alliance',
131
+ mode: 'Standard',
132
+ skills: { archery: 12, tracking: 8 },
133
+ equipment: [
134
+ { slot: 'weapon', itemName: 'Longbow', itemKey: 'items/longbow', rarity: 'Rare' },
135
+ ],
136
+ textureKey: 'pink-hair-girl-1',
137
+ },
138
+ createdAt: daysAgo(2),
139
+ updatedAt: daysAgo(2),
140
+ },
141
+ ];
142
+
143
+ const mockMyCharacterListings: ICharacterListing[] = [
144
+ {
145
+ _id: 'cl-10',
146
+ character: 'char-10',
147
+ seller: 'player-1',
148
+ listedByCharacterName: 'MyMainChar',
149
+ price: 80000,
150
+ isBeingBought: false,
151
+ characterSnapshot: {
152
+ name: 'MyWarrior',
153
+ level: 35,
154
+ class: 'Warrior',
155
+ race: 'Human',
156
+ faction: 'Alliance',
157
+ mode: 'Standard',
158
+ skills: { sword: 18, shield: 16 },
159
+ equipment: [
160
+ { slot: 'weapon', itemName: 'Epic Sword', itemKey: 'items/epic-sword', rarity: 'Epic' },
161
+ ],
162
+ textureKey: 'black-knight',
163
+ },
164
+ createdAt: daysAgo(10),
165
+ updatedAt: daysAgo(10),
166
+ },
167
+ {
168
+ _id: 'cl-11',
169
+ character: 'char-11',
170
+ seller: 'player-1',
171
+ listedByCharacterName: 'MyMainChar',
172
+ price: 25000,
173
+ isBeingBought: true,
174
+ characterSnapshot: {
175
+ name: 'MyAltMage',
176
+ level: 15,
177
+ class: 'Mage',
178
+ race: 'Human',
179
+ faction: 'Alliance',
180
+ mode: 'Standard',
181
+ skills: { fireball: 8 },
182
+ equipment: [
183
+ { slot: 'weapon', itemName: 'Basic Staff', itemKey: 'items/staff', rarity: 'Common' },
184
+ ],
185
+ textureKey: 'red-mage-1',
186
+ },
187
+ createdAt: daysAgo(5),
188
+ updatedAt: daysAgo(1),
189
+ },
190
+ ];
191
+
192
+ export const CharacterBrowse: Story = () => (
193
+ <RPGUIRoot>
194
+ <CharacterMarketplacePanel
195
+ characterListings={mockCharacterListings}
196
+ totalCount={50}
197
+ currentPage={1}
198
+ itemsPerPage={8}
199
+ onPageChange={() => {}}
200
+ onCharacterBuy={() => {}}
201
+ atlasJSON={itemsAtlasJSON}
202
+ atlasIMG={itemsAtlasIMG}
203
+ characterAtlasJSON={entitiesAtlasJSON}
204
+ characterAtlasIMG={entitiesAtlasIMG}
205
+ nameFilter=""
206
+ onNameFilterChange={() => {}}
207
+ isLoading={false}
208
+ />
209
+ </RPGUIRoot>
210
+ );
211
+
212
+ CharacterBrowse.storyName = 'Browse Character Listings';
213
+
214
+ export const CharacterBrowseLoading: Story = () => (
215
+ <RPGUIRoot>
216
+ <CharacterMarketplacePanel
217
+ characterListings={[]}
218
+ totalCount={0}
219
+ currentPage={1}
220
+ itemsPerPage={8}
221
+ onPageChange={() => {}}
222
+ onCharacterBuy={() => {}}
223
+ atlasJSON={itemsAtlasJSON}
224
+ atlasIMG={itemsAtlasIMG}
225
+ characterAtlasJSON={entitiesAtlasJSON}
226
+ characterAtlasIMG={entitiesAtlasIMG}
227
+ nameFilter=""
228
+ onNameFilterChange={() => {}}
229
+ isLoading={true}
230
+ />
231
+ </RPGUIRoot>
232
+ );
233
+
234
+ CharacterBrowseLoading.storyName = 'Loading State';
235
+
236
+ export const CharacterBrowseEmpty: Story = () => (
237
+ <RPGUIRoot>
238
+ <CharacterMarketplacePanel
239
+ characterListings={[]}
240
+ totalCount={0}
241
+ currentPage={1}
242
+ itemsPerPage={8}
243
+ onPageChange={() => {}}
244
+ onCharacterBuy={() => {}}
245
+ atlasJSON={itemsAtlasJSON}
246
+ atlasIMG={itemsAtlasIMG}
247
+ characterAtlasJSON={entitiesAtlasJSON}
248
+ characterAtlasIMG={entitiesAtlasIMG}
249
+ nameFilter=""
250
+ onNameFilterChange={() => {}}
251
+ isLoading={false}
252
+ />
253
+ </RPGUIRoot>
254
+ );
255
+
256
+ CharacterBrowseEmpty.storyName = 'Empty State';
257
+
258
+ export const CharacterBrowseFiltered: Story = () => (
259
+ <RPGUIRoot>
260
+ <CharacterMarketplacePanel
261
+ characterListings={mockCharacterListings.filter(l =>
262
+ l.characterSnapshot.name.toLowerCase().includes('warrior')
263
+ )}
264
+ totalCount={2}
265
+ currentPage={1}
266
+ itemsPerPage={8}
267
+ onPageChange={() => {}}
268
+ onCharacterBuy={() => {}}
269
+ atlasJSON={itemsAtlasJSON}
270
+ atlasIMG={itemsAtlasIMG}
271
+ characterAtlasJSON={entitiesAtlasJSON}
272
+ characterAtlasIMG={entitiesAtlasIMG}
273
+ nameFilter="warrior"
274
+ onNameFilterChange={() => {}}
275
+ isLoading={false}
276
+ />
277
+ </RPGUIRoot>
278
+ );
279
+
280
+ CharacterBrowseFiltered.storyName = 'With Name Filter';
281
+
282
+ export const MyCharacterListings: Story = () => (
283
+ <RPGUIRoot>
284
+ <MyCharacterListingsPanel
285
+ myCharacterListings={mockMyCharacterListings}
286
+ totalCount={2}
287
+ currentPage={1}
288
+ itemsPerPage={8}
289
+ onPageChange={() => {}}
290
+ onCharacterDelist={() => {}}
291
+ atlasJSON={itemsAtlasJSON}
292
+ atlasIMG={itemsAtlasIMG}
293
+ characterAtlasJSON={entitiesAtlasJSON}
294
+ characterAtlasIMG={entitiesAtlasIMG}
295
+ />
296
+ </RPGUIRoot>
297
+ );
298
+
299
+ MyCharacterListings.storyName = 'My Character Listings';
300
+
301
+ export const MyCharacterListingsEmpty: Story = () => (
302
+ <RPGUIRoot>
303
+ <MyCharacterListingsPanel
304
+ myCharacterListings={[]}
305
+ totalCount={0}
306
+ currentPage={1}
307
+ itemsPerPage={8}
308
+ onPageChange={() => {}}
309
+ onCharacterDelist={() => {}}
310
+ atlasJSON={itemsAtlasJSON}
311
+ atlasIMG={itemsAtlasIMG}
312
+ characterAtlasJSON={entitiesAtlasJSON}
313
+ characterAtlasIMG={entitiesAtlasIMG}
314
+ />
315
+ </RPGUIRoot>
316
+ );
317
+
318
+ MyCharacterListingsEmpty.storyName = 'My Listings - Empty State';
319
+
320
+ export const CharacterListingPending: Story = () => (
321
+ <RPGUIRoot>
322
+ <CharacterMarketplacePanel
323
+ characterListings={[mockCharacterListings[2]]} // The one with isBeingBought: true
324
+ totalCount={1}
325
+ currentPage={1}
326
+ itemsPerPage={8}
327
+ onPageChange={() => {}}
328
+ onCharacterBuy={() => {}}
329
+ atlasJSON={itemsAtlasJSON}
330
+ atlasIMG={itemsAtlasIMG}
331
+ characterAtlasJSON={entitiesAtlasJSON}
332
+ characterAtlasIMG={entitiesAtlasIMG}
333
+ nameFilter=""
334
+ onNameFilterChange={() => {}}
335
+ isLoading={false}
336
+ />
337
+ </RPGUIRoot>
338
+ );
339
+
340
+ CharacterListingPending.storyName = 'Listing with Pending Sale';
@@ -0,0 +1,93 @@
1
+ import { Meta, Story } from '@storybook/react';
2
+ import React, { useState } from 'react';
3
+ import { RPGUIRoot } from '../../components/RPGUI/RPGUIRoot';
4
+ import {
5
+ IRadioOptionProps,
6
+ RadioOption,
7
+ RadioOptionLabel,
8
+ RadioOptionSub,
9
+ } from '../../components/shared/RadioOption';
10
+
11
+ const meta: Meta = {
12
+ title: 'Shared/RadioOption',
13
+ component: RadioOption,
14
+ decorators: [
15
+ Story => (
16
+ <RPGUIRoot>
17
+ <div style={{ padding: '2rem', background: '#1a1a2e', minWidth: '320px' }}>
18
+ <Story />
19
+ </div>
20
+ </RPGUIRoot>
21
+ ),
22
+ ],
23
+ };
24
+
25
+ export default meta;
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Single unselected option
29
+ // ---------------------------------------------------------------------------
30
+ export const Unselected: Story<IRadioOptionProps> = () => (
31
+ <RadioOption selected={false} onSelect={() => {}}>
32
+ <div>
33
+ <RadioOptionLabel>Pay with Stripe</RadioOptionLabel>
34
+ </div>
35
+ </RadioOption>
36
+ );
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Single selected option
40
+ // ---------------------------------------------------------------------------
41
+ export const Selected: Story<IRadioOptionProps> = () => (
42
+ <RadioOption selected={true} onSelect={() => {}}>
43
+ <div>
44
+ <RadioOptionLabel>Pay with Stripe</RadioOptionLabel>
45
+ </div>
46
+ </RadioOption>
47
+ );
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Disabled option
51
+ // ---------------------------------------------------------------------------
52
+ export const Disabled: Story<IRadioOptionProps> = () => (
53
+ <RadioOption selected={false} disabled={true} onSelect={() => {}}>
54
+ <div>
55
+ <RadioOptionLabel $disabled={true}>Pay with DC Wallet</RadioOptionLabel>
56
+ <RadioOptionSub>Insufficient balance</RadioOptionSub>
57
+ </div>
58
+ </RadioOption>
59
+ );
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Interactive group of 3 options
63
+ // ---------------------------------------------------------------------------
64
+ const OPTIONS = [
65
+ { id: 'stripe', label: 'Pay with Stripe', sub: 'Credit / debit card' },
66
+ { id: 'dc', label: 'Pay with DC Wallet', sub: '250 DC available' },
67
+ { id: 'paypal', label: 'Pay with PayPal', sub: 'Coming soon' },
68
+ ] as const;
69
+
70
+ type IOptionId = (typeof OPTIONS)[number]['id'];
71
+
72
+ export const InteractiveGroup: Story = () => {
73
+ const [selected, setSelected] = useState<IOptionId>('stripe');
74
+
75
+ return (
76
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
77
+ {OPTIONS.map(opt => (
78
+ <RadioOption
79
+ key={opt.id}
80
+ selected={selected === opt.id}
81
+ onSelect={() => setSelected(opt.id)}
82
+ >
83
+ <div>
84
+ <RadioOptionLabel>{opt.label}</RadioOptionLabel>
85
+ <div style={{ marginTop: '3px' }}>
86
+ <RadioOptionSub>{opt.sub}</RadioOptionSub>
87
+ </div>
88
+ </div>
89
+ </RadioOption>
90
+ ))}
91
+ </div>
92
+ );
93
+ };