@rpg-engine/long-bow 0.8.219 → 0.8.221

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/DraggableContainer.d.ts +0 -6
  2. package/dist/components/Store/CartView.d.ts +0 -2
  3. package/dist/components/Store/MetadataCollector.d.ts +2 -2
  4. package/dist/components/Store/Store.d.ts +10 -28
  5. package/dist/components/Store/StoreHeader.d.ts +14 -0
  6. package/dist/components/Store/hooks/useStoreCart.d.ts +2 -0
  7. package/dist/components/Store/hooks/useStoreMetadata.d.ts +4 -11
  8. package/dist/components/Store/hooks/useStoreTabs.d.ts +20 -0
  9. package/dist/components/Store/internal/packToBlueprint.d.ts +2 -0
  10. package/dist/components/Store/sections/StoreItemsSection.d.ts +5 -3
  11. package/dist/hooks/useStoreFiltering.d.ts +7 -4
  12. package/dist/long-bow.cjs.development.js +379 -441
  13. package/dist/long-bow.cjs.development.js.map +1 -1
  14. package/dist/long-bow.cjs.production.min.js +1 -1
  15. package/dist/long-bow.cjs.production.min.js.map +1 -1
  16. package/dist/long-bow.esm.js +381 -443
  17. package/dist/long-bow.esm.js.map +1 -1
  18. package/package.json +1 -1
  19. package/src/components/DraggableContainer.tsx +0 -24
  20. package/src/components/Store/CartView.tsx +116 -137
  21. package/src/components/Store/MetadataCollector.tsx +60 -40
  22. package/src/components/Store/Store.tsx +75 -285
  23. package/src/components/Store/StoreHeader.tsx +74 -0
  24. package/src/components/Store/__test__/MetadataCollector.spec.tsx +94 -164
  25. package/src/components/Store/__test__/Store.spec.tsx +4 -0
  26. package/src/components/Store/__test__/useStoreMetadata.spec.tsx +58 -156
  27. package/src/components/Store/__test__/useStoreTabs.spec.tsx +69 -0
  28. package/src/components/Store/hooks/useStoreCart.ts +5 -2
  29. package/src/components/Store/hooks/useStoreMetadata.ts +30 -48
  30. package/src/components/Store/hooks/useStoreTabs.ts +104 -0
  31. package/src/components/Store/internal/packToBlueprint.ts +21 -0
  32. package/src/components/Store/sections/StoreItemsSection.tsx +19 -60
  33. package/src/components/Store/sections/StorePacksSection.tsx +0 -1
  34. package/src/components/shared/ScrollableContent/ScrollableContent.tsx +3 -6
  35. package/src/hooks/useStoreFiltering.spec.tsx +79 -0
  36. package/src/hooks/useStoreFiltering.ts +27 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.219",
3
+ "version": "0.8.221",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -12,12 +12,6 @@ export interface IDraggableContainerProps {
12
12
  height?: string;
13
13
  minWidth?: string;
14
14
  minHeight?: string;
15
- /** CSS override for width applied below `mobileBreakpoint`. Useful for responsive sizing without JS detection. */
16
- mobileWidth?: string;
17
- /** CSS override for height applied below `mobileBreakpoint`. Useful for responsive sizing without JS detection. */
18
- mobileHeight?: string;
19
- /** Viewport width in px under which mobile overrides apply. Defaults to 768. */
20
- mobileBreakpoint?: number;
21
15
  className?: string;
22
16
  type?: RPGUIContainerTypes;
23
17
  title?: string;
@@ -42,9 +36,6 @@ export const DraggableContainer: React.FC<IDraggableContainerProps> = ({
42
36
  height,
43
37
  minHeight,
44
38
  minWidth,
45
- mobileWidth,
46
- mobileHeight,
47
- mobileBreakpoint = 768,
48
39
  className,
49
40
  type = RPGUIContainerTypes.FramedGold,
50
41
  onCloseButton,
@@ -126,9 +117,6 @@ export const DraggableContainer: React.FC<IDraggableContainerProps> = ({
126
117
  height={height || 'auto'}
127
118
  minWidth={minWidth}
128
119
  minHeight={minHeight}
129
- mobileWidth={mobileWidth}
130
- mobileHeight={mobileHeight}
131
- mobileBreakpoint={mobileBreakpoint}
132
120
  className={`rpgui-container ${type} ${className}`}
133
121
  isFullScreen={isFullScreen}
134
122
  opacity={opacity}
@@ -161,9 +149,6 @@ interface IContainerProps {
161
149
  height: string;
162
150
  minWidth?: string;
163
151
  minHeight?: string;
164
- mobileWidth?: string;
165
- mobileHeight?: string;
166
- mobileBreakpoint?: number;
167
152
  isFullScreen?: boolean;
168
153
  opacity?: number;
169
154
  }
@@ -193,15 +178,6 @@ const Container = styled.div<IContainerProps>`
193
178
  align-content: flex-start;
194
179
  `}
195
180
 
196
- ${({ mobileWidth, mobileHeight, mobileBreakpoint }) =>
197
- (mobileWidth || mobileHeight) &&
198
- css`
199
- @media (max-width: ${mobileBreakpoint ?? 768}px) {
200
- ${mobileWidth && `width: ${mobileWidth}; min-width: unset;`}
201
- ${mobileHeight && `height: ${mobileHeight}; min-height: unset;`}
202
- }
203
- `}
204
-
205
181
  &.rpgui-container {
206
182
  padding-top: 1.5rem;
207
183
  }
@@ -1,6 +1,6 @@
1
1
  import { IProductBlueprint, MetadataType } from '@rpg-engine/shared';
2
2
  import React, { useState } from 'react';
3
- import { FaCoins, FaInfoCircle, FaShoppingBag, FaTimes, FaTrash } from 'react-icons/fa';
3
+ import { FaInfoCircle, FaShoppingBag, FaTimes, FaTrash } from 'react-icons/fa';
4
4
  import styled from 'styled-components';
5
5
  import characterAtlasJSON from '../../mocks/atlas/entities/entities.json';
6
6
  import characterAtlasIMG from '../../mocks/atlas/entities/entities.png';
@@ -27,8 +27,6 @@ export interface ICartViewProps {
27
27
  paymentMethodLabel?: string;
28
28
  trustSignals?: ITrustSignal[];
29
29
  onCloseStore?: () => void;
30
- /** Called when user taps the "Buy DC" nudge — open wallet/DC purchase flow */
31
- onBuyDC?: () => void;
32
30
  /** Fires when user taps the pay button — before the purchase resolves */
33
31
  onCheckoutStart?: (items: Array<{ key: string; name: string; quantity: number }>, total: number) => void;
34
32
  /** Fires after a successful purchase */
@@ -69,7 +67,6 @@ export const CartView: React.FC<ICartViewProps> = ({
69
67
  paymentMethodLabel,
70
68
  trustSignals,
71
69
  onCloseStore,
72
- onBuyDC,
73
70
  onCheckoutStart,
74
71
  onPurchaseSuccess,
75
72
  onPurchaseError,
@@ -118,10 +115,6 @@ export const CartView: React.FC<ICartViewProps> = ({
118
115
  }
119
116
  };
120
117
 
121
- // Show DC discount nudge when items have DC pricing and user might benefit
122
- const hasDCItems = cartItems.some(ci => (ci.item as any).dcPrice);
123
- const showDCNudge = hasDCItems && onBuyDC;
124
-
125
118
  if (purchasedItems) {
126
119
  return (
127
120
  <PurchaseSuccess
@@ -150,100 +143,95 @@ export const CartView: React.FC<ICartViewProps> = ({
150
143
  </CloseButton>
151
144
  </Header>
152
145
 
153
- <CartItems>
154
- {cartItems.length === 0 ? (
155
- <EmptyCart>Your cart is empty</EmptyCart>
156
- ) : (
157
- cartItems.map(cartItem => {
158
- console.log('Item metadataType: , texturePath:', cartItem.item.metadataType , cartItem.item.texturePath);
159
- const getSpriteKey = (textureKey: string) => {
160
- return textureKey + '/down/standing/0.png';
161
- };
162
-
163
- return (
164
- <CartItemRow key={cartItem.item.key}>
165
- <ItemIconContainer>
166
- <SpriteFromAtlas
167
- atlasJSON={cartItem.item.metadataType === MetadataType.CharacterSkin ? characterAtlasJSON : atlasJSON}
168
- atlasIMG={cartItem.item.metadataType === MetadataType.CharacterSkin ? characterAtlasIMG : atlasIMG}
169
- spriteKey={cartItem.item.metadataType === MetadataType.CharacterSkin && cartItem.metadata?.selectedSkinTextureKey ? getSpriteKey(cartItem.metadata.selectedSkinTextureKey) : cartItem.item.texturePath}
170
- width={24}
171
- height={24}
172
- imgScale={1.5}
173
- centered
174
- />
175
- </ItemIconContainer>
176
- <ItemDetails>
177
- <ItemName>{cartItem.item.name}</ItemName>
178
- {cartItem.metadata?.inputValue && (
179
- <CartMeta>{cartItem.metadata.inputValue}</CartMeta>
180
- )}
181
- <ItemInfo>
182
- <span>{currencySymbol}{formatPrice((cartItem.item as any).regionalPrice ?? cartItem.item.price)}</span>
183
- <span>×</span>
184
- <span>{cartItem.quantity}</span>
185
- <span>=</span>
186
- <span>
187
- {currencySymbol}{formatPrice(((cartItem.item as any).regionalPrice ?? cartItem.item.price) * cartItem.quantity)}
188
- </span>
189
- </ItemInfo>
190
-
191
- {cartItem.metadata && cartItem.item.metadataType && (
192
- <MetadataDisplay
193
- type={cartItem.item.metadataType}
194
- metadata={cartItem.metadata}
146
+ <MainContent>
147
+ <CartItems>
148
+ {cartItems.length === 0 ? (
149
+ <EmptyCart>Your cart is empty</EmptyCart>
150
+ ) : (
151
+ cartItems.map(cartItem => {
152
+ const getSpriteKey = (textureKey: string) => {
153
+ return textureKey + '/down/standing/0.png';
154
+ };
155
+
156
+ return (
157
+ <CartItemRow key={cartItem.item.key}>
158
+ <ItemIconContainer>
159
+ <SpriteFromAtlas
160
+ atlasJSON={cartItem.item.metadataType === MetadataType.CharacterSkin ? characterAtlasJSON : atlasJSON}
161
+ atlasIMG={cartItem.item.metadataType === MetadataType.CharacterSkin ? characterAtlasIMG : atlasIMG}
162
+ spriteKey={cartItem.item.metadataType === MetadataType.CharacterSkin && cartItem.metadata?.selectedSkinTextureKey ? getSpriteKey(cartItem.metadata.selectedSkinTextureKey) : cartItem.item.texturePath}
163
+ width={24}
164
+ height={24}
165
+ imgScale={1.5}
166
+ centered
195
167
  />
196
- )}
197
- </ItemDetails>
198
-
199
- <CTAButton
200
- icon={<FaTrash />}
201
- onClick={e => {
202
- e.stopPropagation();
203
- onRemoveFromCart(cartItem.item.key);
204
- }}
205
- />
206
- </CartItemRow>
207
- );
208
- })
209
- )}
210
- </CartItems>
168
+ </ItemIconContainer>
169
+ <ItemDetails>
170
+ <ItemName>{cartItem.item.name}</ItemName>
171
+ {cartItem.metadata?.inputValue && (
172
+ <CartMeta>{cartItem.metadata.inputValue}</CartMeta>
173
+ )}
174
+ <ItemInfo>
175
+ <span>{currencySymbol}{formatPrice((cartItem.item as any).regionalPrice ?? cartItem.item.price)}</span>
176
+ <span>×</span>
177
+ <span>{cartItem.quantity}</span>
178
+ <span>=</span>
179
+ <span>
180
+ {currencySymbol}{formatPrice(((cartItem.item as any).regionalPrice ?? cartItem.item.price) * cartItem.quantity)}
181
+ </span>
182
+ </ItemInfo>
183
+
184
+ {cartItem.metadata && cartItem.item.metadataType && (
185
+ <MetadataDisplay
186
+ type={cartItem.item.metadataType}
187
+ metadata={cartItem.metadata}
188
+ />
189
+ )}
190
+ </ItemDetails>
191
+
192
+ <CTAButton
193
+ icon={<FaTrash />}
194
+ onClick={e => {
195
+ e.stopPropagation();
196
+ onRemoveFromCart(cartItem.item.key);
197
+ }}
198
+ />
199
+ </CartItemRow>
200
+ );
201
+ })
202
+ )}
203
+ </CartItems>
211
204
 
212
- <Footer>
213
- {showDCNudge && (
214
- <DCNudge onPointerDown={onBuyDC}>
215
- <FaCoins />
216
- <span>Save more with DC — volume discounts available</span>
217
- <DCNudgeLink>Buy DC →</DCNudgeLink>
218
- </DCNudge>
205
+ {cartItems.length > 0 && (
206
+ <OrderSummaryPanel>
207
+ <OrderSummaryLabel>Order Summary</OrderSummaryLabel>
208
+ <TotalRow>
209
+ <span>Subtotal:</span>
210
+ <span>{currencySymbol}{formatPrice(total)}</span>
211
+ </TotalRow>
212
+ {dcTotal > 0 && (
213
+ <TotalRow>
214
+ <span>DC:</span>
215
+ <span><MMORPGNumber value={dcTotal} /> DC</span>
216
+ </TotalRow>
217
+ )}
218
+ <TotalRow $isTotal>
219
+ <span>Total:</span>
220
+ <span>{currencySymbol}{formatPrice(total)}</span>
221
+ </TotalRow>
222
+ {paymentMethodLabel && (
223
+ <PaymentMethodRow>
224
+ <span>Paying with:</span>
225
+ <span>{paymentMethodLabel}</span>
226
+ </PaymentMethodRow>
227
+ )}
228
+ </OrderSummaryPanel>
219
229
  )}
230
+ </MainContent>
220
231
 
232
+ <Footer>
221
233
  <TrustBar signals={trustSignals} />
222
-
223
- <TotalInfo>
224
- <OrderSummaryLabel>Order Summary</OrderSummaryLabel>
225
- <TotalRow>
226
- <span>Subtotal:</span>
227
- <span>{currencySymbol}{formatPrice(total)}</span>
228
- </TotalRow>
229
- {dcTotal > 0 && (
230
- <TotalRow>
231
- <span>DC:</span>
232
- <span><MMORPGNumber value={dcTotal} /> DC</span>
233
- </TotalRow>
234
- )}
235
- <TotalRow $isTotal>
236
- <span>Total:</span>
237
- <span>{currencySymbol}{formatPrice(total)}</span>
238
- </TotalRow>
239
- {paymentMethodLabel && (
240
- <PaymentMethodRow>
241
- <span>Paying with:</span>
242
- <span>{paymentMethodLabel}</span>
243
- </PaymentMethodRow>
244
- )}
245
- {error && <ErrorMessage>{error}</ErrorMessage>}
246
- </TotalInfo>
234
+ {error && <ErrorMessage>{error}</ErrorMessage>}
247
235
  <CTAButton
248
236
  icon={<FaShoppingBag />}
249
237
  label={isLoading ? 'Processing...' : `Pay ${currencySymbol}${formatPrice(total)}`}
@@ -260,14 +248,17 @@ const Container = styled.div`
260
248
  display: flex;
261
249
  flex-direction: column;
262
250
  width: 100%;
263
- gap: 1rem;
251
+ height: 100%;
264
252
  padding: 1rem;
253
+ overflow: hidden;
254
+ box-sizing: border-box;
265
255
  `;
266
256
 
267
257
  const Header = styled.div`
268
258
  display: flex;
269
259
  justify-content: space-between;
270
260
  align-items: center;
261
+ flex-shrink: 0;
271
262
  `;
272
263
 
273
264
  const Title = styled.h2`
@@ -289,12 +280,26 @@ const CloseButton = styled.div`
289
280
  justify-content: center;
290
281
  `;
291
282
 
283
+ const MainContent = styled.div`
284
+ display: grid;
285
+ grid-template-columns: 1fr 260px;
286
+ gap: 1rem;
287
+ flex: 1;
288
+ min-height: 0;
289
+ margin: 1rem 0;
290
+
291
+ @media (max-width: 700px) {
292
+ grid-template-columns: 1fr;
293
+ gap: 0.75rem;
294
+ }
295
+ `;
296
+
292
297
  const CartItems = styled.div`
293
298
  display: flex;
294
299
  flex-direction: column;
295
300
  gap: 0.5rem;
301
+ min-height: 0;
296
302
  overflow-y: auto;
297
- max-height: 250px;
298
303
  padding-right: 0.5rem;
299
304
 
300
305
  &::-webkit-scrollbar {
@@ -307,13 +312,24 @@ const CartItems = styled.div`
307
312
  &::-webkit-scrollbar-thumb {
308
313
  background: #f59e0b;
309
314
  border-radius: 4px;
310
-
315
+
311
316
  &:hover {
312
317
  background: #fbbf24;
313
318
  }
314
319
  }
315
320
  `;
316
321
 
322
+ const OrderSummaryPanel = styled.div`
323
+ display: flex;
324
+ flex-direction: column;
325
+ gap: 0.5rem;
326
+ padding: 1rem;
327
+ background: rgba(0, 0, 0, 0.25);
328
+ border: 1px solid rgba(255, 255, 255, 0.08);
329
+ border-radius: 6px;
330
+ align-self: start;
331
+ `;
332
+
317
333
  const EmptyCart = styled.div`
318
334
  display: flex;
319
335
  align-items: center;
@@ -380,12 +396,6 @@ const Footer = styled.div`
380
396
  }
381
397
  `;
382
398
 
383
- const TotalInfo = styled.div`
384
- display: flex;
385
- flex-direction: column;
386
- gap: 0.5rem;
387
- `;
388
-
389
399
  const OrderSummaryLabel = styled.div`
390
400
  font-family: 'Press Start 2P', cursive;
391
401
  font-size: 0.55rem;
@@ -429,37 +439,6 @@ const PaymentMethodRow = styled.div`
429
439
  }
430
440
  `;
431
441
 
432
- const DCNudge = styled.div`
433
- display: flex;
434
- align-items: center;
435
- gap: 0.5rem;
436
- padding: 0.5rem 0.75rem;
437
- background: rgba(245, 158, 11, 0.1);
438
- border: 1px solid rgba(245, 158, 11, 0.3);
439
- border-radius: 4px;
440
- cursor: pointer;
441
- font-family: 'Press Start 2P', cursive;
442
- font-size: 0.45rem;
443
- color: #fbbf24;
444
- transition: background 0.15s;
445
- line-height: 1.4;
446
- flex-wrap: wrap;
447
-
448
- &:hover {
449
- background: rgba(245, 158, 11, 0.18);
450
- }
451
-
452
- svg { flex-shrink: 0; font-size: 0.7rem; }
453
- span { flex: 1; min-width: 140px; }
454
- `;
455
-
456
- const DCNudgeLink = styled.span`
457
- color: #f59e0b;
458
- white-space: nowrap;
459
- text-decoration: underline;
460
- flex: 0 0 auto !important;
461
- `;
462
-
463
442
  const ErrorMessage = styled.div`
464
443
  color: #ef4444;
465
444
  font-size: 0.875rem;
@@ -1,48 +1,68 @@
1
- import { MetadataType } from "@rpg-engine/shared";
2
- import React, { useEffect } from "react";
3
- import { CharacterSkinSelectionModal } from "../Character/CharacterSkinSelectionModal";
1
+ import { MetadataType } from '@rpg-engine/shared';
2
+ import React, { useCallback, useEffect, useRef } from 'react';
3
+ import { CharacterSkinSelectionModal } from '../Character/CharacterSkinSelectionModal';
4
4
 
5
5
  export interface IMetadataCollectorProps {
6
- metadataType: MetadataType;
7
- config: Record<string, any>;
8
- onCollect: (metadata: Record<string, any>) => void;
9
- onCancel: () => void;
6
+ metadataType: MetadataType;
7
+ config: Record<string, any>;
8
+ onCollect: (metadata: Record<string, any>) => void;
9
+ onCancel: () => void;
10
10
  }
11
11
 
12
12
  export const MetadataCollector: React.FC<IMetadataCollectorProps> = ({
13
- metadataType,
14
- config,
15
- onCollect,
16
- onCancel,
13
+ metadataType,
14
+ config,
15
+ onCollect,
16
+ onCancel,
17
17
  }) => {
18
- // Make sure we clean up if unmounted without collecting
19
- useEffect(() => {
20
- return () => {
21
- // If we're unmounting without explicitly collecting or canceling,
22
- // make sure to call onCancel to prevent any hanging promises
23
- if (window.__metadataResolvers) {
24
- onCancel();
25
- }
26
- };
27
- }, [onCancel]);
28
-
29
- // Use string comparison instead of direct property access
30
- if (metadataType === 'CharacterSkin') {
31
- return (
32
- <CharacterSkinSelectionModal
33
- isOpen={true}
34
- onClose={onCancel}
35
- onConfirm={(selectedSkin: any) => onCollect({ selectedSkin })}
36
- availableCharacters={config.availableCharacters || []}
37
- atlasJSON={config.atlasJSON}
38
- atlasIMG={config.atlasIMG}
39
- initialSelectedSkin={config.initialSelectedSkin}
40
- />
41
- );
42
- } else {
43
- console.warn(`No collector implemented for metadata type: ${metadataType}`);
44
- // Auto-cancel for unhandled types to prevent hanging promises
45
- setTimeout(onCancel, 0);
46
- return null;
18
+ const isPendingRef = useRef(true);
19
+
20
+ const finalize = useCallback((callback: () => void) => {
21
+ if (!isPendingRef.current) {
22
+ return;
23
+ }
24
+
25
+ isPendingRef.current = false;
26
+ callback();
27
+ }, []);
28
+
29
+ const handleCollect = useCallback((metadata: Record<string, any>) => {
30
+ finalize(() => onCollect(metadata));
31
+ }, [finalize, onCollect]);
32
+
33
+ const handleCancel = useCallback(() => {
34
+ finalize(onCancel);
35
+ }, [finalize, onCancel]);
36
+
37
+ useEffect(() => {
38
+ return () => {
39
+ handleCancel();
40
+ };
41
+ }, [handleCancel]);
42
+
43
+ useEffect(() => {
44
+ if (metadataType === MetadataType.CharacterSkin) {
45
+ return undefined;
47
46
  }
47
+
48
+ const timer = window.setTimeout(handleCancel, 0);
49
+ return () => window.clearTimeout(timer);
50
+ }, [handleCancel, metadataType]);
51
+
52
+ if (metadataType === MetadataType.CharacterSkin) {
53
+ return (
54
+ <CharacterSkinSelectionModal
55
+ isOpen
56
+ onClose={handleCancel}
57
+ onConfirm={(selectedSkin: any) => handleCollect({ selectedSkin })}
58
+ availableCharacters={config.availableCharacters || []}
59
+ atlasJSON={config.atlasJSON}
60
+ atlasIMG={config.atlasIMG}
61
+ initialSelectedSkin={config.initialSelectedSkin}
62
+ />
63
+ );
64
+ }
65
+
66
+ console.warn(`No collector implemented for metadata type: ${metadataType}`);
67
+ return null;
48
68
  };